@@ -53,8 +53,8 @@ INVITATION_CODE=try-huginn |
||
53 | 53 |
|
54 | 54 |
# Outgoing email settings. To use Gmail or Google Apps, put your Google Apps domain or gmail.com |
55 | 55 |
# as the SMTP_DOMAIN and your Gmail username and password as the SMTP_USER_NAME and SMTP_PASSWORD. |
56 |
-# |
|
57 |
-# PLEASE NOTE: In order to enable emails locally (e.g., when not in the production Rails environment), |
|
56 |
+# |
|
57 |
+# PLEASE NOTE: In order to enable emails locally (e.g., when not in the production Rails environment), |
|
58 | 58 |
# you must also change config.action_mailer.perform_deliveries in config/environments/development.rb. |
59 | 59 |
|
60 | 60 |
SMTP_DOMAIN=your-domain-here.com |
@@ -92,6 +92,9 @@ GITHUB_OAUTH_SECRET= |
||
92 | 92 |
TUMBLR_OAUTH_KEY= |
93 | 93 |
TUMBLR_OAUTH_SECRET= |
94 | 94 |
|
95 |
+DROPBOX_OAUTH_KEY= |
|
96 |
+DROPBOX_OAUTH_SECRET= |
|
97 |
+ |
|
95 | 98 |
############################# |
96 | 99 |
# AWS and Mechanical Turk # |
97 | 100 |
############################# |
@@ -132,6 +135,11 @@ ENABLE_INSECURE_AGENTS=false |
||
132 | 135 |
# "on the minute") is disallowed to prevent abuse of service. |
133 | 136 |
ENABLE_SECOND_PRECISION_SCHEDULE=false |
134 | 137 |
|
138 |
+# Specify the scheduler frequency in seconds (default: 0.3). |
|
139 |
+# Increasing this value will help reduce the use of system resources |
|
140 |
+# at the expense of time accuracy. |
|
141 |
+#SCHEDULER_FREQUENCY=0.3 |
|
142 |
+ |
|
135 | 143 |
# Use Graphviz for generating diagrams instead of using Google Chart |
136 | 144 |
# Tools. Specify a dot(1) command path built with SVG support |
137 | 145 |
# enabled. |
@@ -26,6 +26,10 @@ gem 'omniauth-twitter' |
||
26 | 26 |
gem 'tumblr_client' |
27 | 27 |
gem 'omniauth-tumblr' |
28 | 28 |
|
29 |
+# Dropbox Agents |
|
30 |
+gem 'dropbox-api' |
|
31 |
+gem 'omniauth-dropbox' |
|
32 |
+ |
|
29 | 33 |
# Optional Services. |
30 | 34 |
gem 'omniauth-37signals' # BasecampAgent |
31 | 35 |
# gem 'omniauth-github' |
@@ -46,7 +50,7 @@ gem 'coffee-rails', '~> 4.0.0' |
||
46 | 50 |
gem 'daemons', '~> 1.1.9' |
47 | 51 |
gem 'delayed_job', '~> 4.0.0' |
48 | 52 |
gem 'delayed_job_active_record', '~> 4.0.0' |
49 |
-gem 'devise', '~> 3.2.4' |
|
53 |
+gem 'devise', '~> 3.4.0' |
|
50 | 54 |
gem 'em-http-request', '~> 1.1.2' |
51 | 55 |
gem 'faraday', '~> 0.9.0' |
52 | 56 |
gem 'faraday_middleware' |
@@ -81,6 +85,9 @@ group :development do |
||
81 | 85 |
gem 'better_errors', '~> 1.1' |
82 | 86 |
gem 'binding_of_caller' |
83 | 87 |
gem 'quiet_assets' |
88 |
+ gem 'guard' |
|
89 |
+ gem 'guard-livereload' |
|
90 |
+ gem 'guard-rspec' |
|
84 | 91 |
end |
85 | 92 |
|
86 | 93 |
group :development, :test do |
@@ -92,6 +99,7 @@ group :development, :test do |
||
92 | 99 |
gem 'rspec', '~> 3.0' |
93 | 100 |
gem 'rspec-collection_matchers', '~> 1.0.0' |
94 | 101 |
gem 'rspec-rails', '~> 3.0.1' |
102 |
+ gem 'rspec-html-matchers', '~> 0.6.1' |
|
95 | 103 |
gem 'shoulda-matchers' |
96 | 104 |
gem 'spring' |
97 | 105 |
gem 'spring-commands-rspec' |
@@ -55,6 +55,8 @@ GEM |
||
55 | 55 |
rails (>= 3.1) |
56 | 56 |
buftok (0.2.0) |
57 | 57 |
builder (3.2.2) |
58 |
+ celluloid (0.15.2) |
|
59 |
+ timers (~> 1.1.0) |
|
58 | 60 |
chronic (0.10.2) |
59 | 61 |
coderay (1.1.0) |
60 | 62 |
coffee-rails (4.0.1) |
@@ -82,10 +84,11 @@ GEM |
||
82 | 84 |
delayed_job (>= 3.0, < 4.1) |
83 | 85 |
delorean (2.1.0) |
84 | 86 |
chronic |
85 |
- devise (3.2.4) |
|
87 |
+ devise (3.4.0) |
|
86 | 88 |
bcrypt (~> 3.0) |
87 | 89 |
orm_adapter (~> 0.1) |
88 | 90 |
railties (>= 3.2.6, < 5) |
91 |
+ responders |
|
89 | 92 |
thread_safe (~> 0.1) |
90 | 93 |
warden (~> 1.2.3) |
91 | 94 |
diff-lcs (1.2.5) |
@@ -95,6 +98,10 @@ GEM |
||
95 | 98 |
dotenv-deployment (0.0.2) |
96 | 99 |
dotenv-rails (0.11.1) |
97 | 100 |
dotenv (= 0.11.1) |
101 |
+ dropbox-api (0.4.2) |
|
102 |
+ hashie |
|
103 |
+ multi_json |
|
104 |
+ oauth |
|
98 | 105 |
em-http-request (1.1.2) |
99 | 106 |
addressable (>= 2.3.4) |
100 | 107 |
cookiejar |
@@ -103,6 +110,9 @@ GEM |
||
103 | 110 |
http_parser.rb (>= 0.6.0) |
104 | 111 |
em-socksify (0.3.0) |
105 | 112 |
eventmachine (>= 1.0.0.beta.4) |
113 |
+ em-websocket (0.5.1) |
|
114 |
+ eventmachine (>= 0.12.9) |
|
115 |
+ http_parser.rb (~> 0.6.0) |
|
106 | 116 |
equalizer (0.0.9) |
107 | 117 |
erector (0.10.0) |
108 | 118 |
treetop (>= 1.2.3) |
@@ -129,6 +139,7 @@ GEM |
||
129 | 139 |
foreman (0.63.0) |
130 | 140 |
dotenv (>= 0.7) |
131 | 141 |
thor (>= 0.13.6) |
142 |
+ formatador (0.2.5) |
|
132 | 143 |
geokit (1.8.5) |
133 | 144 |
multi_json (>= 1.3.2) |
134 | 145 |
geokit-rails (2.0.1) |
@@ -145,6 +156,19 @@ GEM |
||
145 | 156 |
retriable (>= 1.4) |
146 | 157 |
signet (>= 0.5.0) |
147 | 158 |
uuidtools (>= 2.1.0) |
159 |
+ guard (2.6.1) |
|
160 |
+ formatador (>= 0.2.4) |
|
161 |
+ listen (~> 2.7) |
|
162 |
+ lumberjack (~> 1.0) |
|
163 |
+ pry (>= 0.9.12) |
|
164 |
+ thor (>= 0.18.1) |
|
165 |
+ guard-livereload (2.2.0) |
|
166 |
+ em-websocket (~> 0.5) |
|
167 |
+ guard (~> 2.0) |
|
168 |
+ multi_json (~> 1.8) |
|
169 |
+ guard-rspec (4.3.1) |
|
170 |
+ guard (~> 2.1) |
|
171 |
+ rspec (>= 2.14, < 4.0) |
|
148 | 172 |
hashie (2.0.5) |
149 | 173 |
hike (1.2.3) |
150 | 174 |
hipchat (1.2.0) |
@@ -171,8 +195,13 @@ GEM |
||
171 | 195 |
kramdown (1.3.3) |
172 | 196 |
launchy (2.4.2) |
173 | 197 |
addressable (~> 2.3) |
174 |
- libv8 (3.16.14.3) |
|
198 |
+ libv8 (3.16.14.7) |
|
175 | 199 |
liquid (2.6.1) |
200 |
+ listen (2.7.9) |
|
201 |
+ celluloid (>= 0.15.2) |
|
202 |
+ rb-fsevent (>= 0.9.3) |
|
203 |
+ rb-inotify (>= 0.9) |
|
204 |
+ lumberjack (1.0.9) |
|
176 | 205 |
macaddr (1.7.1) |
177 | 206 |
systemu (~> 2.6.2) |
178 | 207 |
mail (2.5.4) |
@@ -184,7 +213,7 @@ GEM |
||
184 | 213 |
mime-types (1.25.1) |
185 | 214 |
mini_portile (0.6.0) |
186 | 215 |
minitest (5.4.0) |
187 |
- mqtt (0.2.0) |
|
216 |
+ mqtt (0.3.1) |
|
188 | 217 |
multi_json (1.10.1) |
189 | 218 |
multi_xml (0.5.5) |
190 | 219 |
multipart-post (2.0.0) |
@@ -206,6 +235,8 @@ GEM |
||
206 | 235 |
omniauth-37signals (1.0.5) |
207 | 236 |
omniauth (~> 1.0) |
208 | 237 |
omniauth-oauth2 (~> 1.0) |
238 |
+ omniauth-dropbox (0.2.0) |
|
239 |
+ omniauth-oauth (~> 1.0) |
|
209 | 240 |
omniauth-oauth (1.0.1) |
210 | 241 |
oauth |
211 | 242 |
omniauth (~> 1.0) |
@@ -255,9 +286,14 @@ GEM |
||
255 | 286 |
thor (>= 0.18.1, < 2.0) |
256 | 287 |
raindrops (0.13.0) |
257 | 288 |
rake (10.3.2) |
289 |
+ rb-fsevent (0.9.4) |
|
290 |
+ rb-inotify (0.9.5) |
|
291 |
+ ffi (>= 0.5.0) |
|
258 | 292 |
rdoc (4.1.1) |
259 | 293 |
json (~> 1.4) |
260 | 294 |
ref (1.0.5) |
295 |
+ responders (1.1.1) |
|
296 |
+ railties (>= 3.2, < 4.2) |
|
261 | 297 |
rest-client (1.6.8) |
262 | 298 |
mime-types (~> 1.16) |
263 | 299 |
rdoc (>= 2.4.2) |
@@ -274,6 +310,9 @@ GEM |
||
274 | 310 |
rspec-expectations (3.0.4) |
275 | 311 |
diff-lcs (>= 1.2.0, < 2.0) |
276 | 312 |
rspec-support (~> 3.0.0) |
313 |
+ rspec-html-matchers (0.6.1) |
|
314 |
+ nokogiri (~> 1) |
|
315 |
+ rspec (~> 3) |
|
277 | 316 |
rspec-mocks (3.0.4) |
278 | 317 |
rspec-support (~> 3.0.0) |
279 | 318 |
rspec-rails (3.0.2) |
@@ -341,6 +380,7 @@ GEM |
||
341 | 380 |
thor (0.19.1) |
342 | 381 |
thread_safe (0.3.4) |
343 | 382 |
tilt (1.4.1) |
383 |
+ timers (1.1.0) |
|
344 | 384 |
tins (1.3.2) |
345 | 385 |
treetop (1.4.15) |
346 | 386 |
polyglot |
@@ -414,9 +454,10 @@ DEPENDENCIES |
||
414 | 454 |
delayed_job (~> 4.0.0) |
415 | 455 |
delayed_job_active_record (~> 4.0.0) |
416 | 456 |
delorean |
417 |
- devise (~> 3.2.4) |
|
457 |
+ devise (~> 3.4.0) |
|
418 | 458 |
dotenv-deployment |
419 | 459 |
dotenv-rails |
460 |
+ dropbox-api |
|
420 | 461 |
em-http-request (~> 1.1.2) |
421 | 462 |
faraday (~> 0.9.0) |
422 | 463 |
faraday_middleware |
@@ -428,6 +469,9 @@ DEPENDENCIES |
||
428 | 469 |
geokit (~> 1.8.4) |
429 | 470 |
geokit-rails (~> 2.0.1) |
430 | 471 |
google-api-client |
472 |
+ guard |
|
473 |
+ guard-livereload |
|
474 |
+ guard-rspec |
|
431 | 475 |
hipchat (~> 1.2.0) |
432 | 476 |
httparty (~> 0.13) |
433 | 477 |
jquery-rails (~> 3.1.0) |
@@ -443,6 +487,7 @@ DEPENDENCIES |
||
443 | 487 |
nokogiri (~> 1.6.1) |
444 | 488 |
omniauth |
445 | 489 |
omniauth-37signals |
490 |
+ omniauth-dropbox |
|
446 | 491 |
omniauth-tumblr |
447 | 492 |
omniauth-twitter |
448 | 493 |
pg |
@@ -455,6 +500,7 @@ DEPENDENCIES |
||
455 | 500 |
rr |
456 | 501 |
rspec (~> 3.0) |
457 | 502 |
rspec-collection_matchers (~> 1.0.0) |
503 |
+ rspec-html-matchers (~> 0.6.1) |
|
458 | 504 |
rspec-rails (~> 3.0.1) |
459 | 505 |
rturk (~> 2.12.1) |
460 | 506 |
ruby-growl (~> 4.1.0) |
@@ -0,0 +1,25 @@ |
||
1 |
+ |
|
2 |
+guard 'livereload' do |
|
3 |
+ watch(%r{app/views/.+\.(erb|haml|slim)$}) |
|
4 |
+ watch(%r{app/helpers/.+\.rb}) |
|
5 |
+ watch(%r{public/.+\.(css|js|html)}) |
|
6 |
+ watch(%r{config/locales/.+\.yml}) |
|
7 |
+ # Rails Assets Pipeline |
|
8 |
+ watch(%r{(app|vendor)(/assets/\w+/(.+\.(css|js|html|png|jpg))).*}) { |m| "/assets/#{m[3]}" } |
|
9 |
+end |
|
10 |
+ |
|
11 |
+guard :rspec, cmd: 'bundle exec spring rspec' do |
|
12 |
+ watch(%r{^spec/.+_spec\.rb$}) |
|
13 |
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } |
|
14 |
+ watch('spec/spec_helper.rb') { "spec" } |
|
15 |
+ |
|
16 |
+ # Rails example |
|
17 |
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } |
|
18 |
+ watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } |
|
19 |
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } |
|
20 |
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" } |
|
21 |
+ watch('config/routes.rb') { "spec/routing" } |
|
22 |
+ watch('app/controllers/application_controller.rb') { "spec/controllers" } |
|
23 |
+ watch('spec/rails_helper.rb') { "spec" } |
|
24 |
+end |
|
25 |
+ |
@@ -5,6 +5,7 @@ |
||
5 | 5 |
#= require select2 |
6 | 6 |
#= require json2 |
7 | 7 |
#= require jquery.json-editor |
8 |
+#= require jquery.serializeObject |
|
8 | 9 |
#= require latlon_and_geo |
9 | 10 |
#= require spectrum |
10 | 11 |
#= require_tree ./components |
@@ -0,0 +1,76 @@ |
||
1 |
+$ -> |
|
2 |
+ getFormData = (elem) -> |
|
3 |
+ form_data = $("#edit_agent, #new_agent").serializeObject() |
|
4 |
+ attribute = $(elem).data('attribute') |
|
5 |
+ form_data['attribute'] = attribute |
|
6 |
+ delete form_data['_method'] |
|
7 |
+ form_data |
|
8 |
+ |
|
9 |
+ window.initializeFormCompletable = -> |
|
10 |
+ returnedResults = {} |
|
11 |
+ completableDefaultOptions = (input) -> |
|
12 |
+ results: [ |
|
13 |
+ (returnedResults[$(input).data('attribute')] || {text: 'Options', children: [{id: undefined, text: 'loading ...'}]}), |
|
14 |
+ { |
|
15 |
+ text: 'Current', |
|
16 |
+ children: [id: $(input).val(), text: $(input).val()] |
|
17 |
+ }, |
|
18 |
+ { |
|
19 |
+ text: 'Custom', |
|
20 |
+ children: [id: 'manualInput', text: 'manual input'] |
|
21 |
+ }, |
|
22 |
+ ] |
|
23 |
+ |
|
24 |
+ $("input[role~=validatable], select[role~=validatable]").on 'change', (e) => |
|
25 |
+ form_data = getFormData(e.currentTarget) |
|
26 |
+ form_group = $(e.currentTarget).closest('.form-group') |
|
27 |
+ $.ajax '/agents/validate', |
|
28 |
+ type: 'POST', |
|
29 |
+ data: form_data |
|
30 |
+ success: (data) -> |
|
31 |
+ form_group.addClass('has-feedback').removeClass('has-error') |
|
32 |
+ form_group.find('span').addClass('hidden') |
|
33 |
+ form_group.find('.glyphicon-ok').removeClass('hidden') |
|
34 |
+ returnedResults = {} |
|
35 |
+ error: (data) -> |
|
36 |
+ form_group.addClass('has-feedback').addClass('has-error') |
|
37 |
+ form_group.find('span').addClass('hidden') |
|
38 |
+ form_group.find('.glyphicon-remove').removeClass('hidden') |
|
39 |
+ returnedResults = {} |
|
40 |
+ |
|
41 |
+ $("input[role~=validatable], select[role~=validatable]").trigger('change') |
|
42 |
+ |
|
43 |
+ $.each $("input[role~=completable]"), (i, input) -> |
|
44 |
+ $(input).select2( |
|
45 |
+ data: -> |
|
46 |
+ completableDefaultOptions(input) |
|
47 |
+ ).on("change", (e) -> |
|
48 |
+ if e.added && e.added.id == 'manualInput' |
|
49 |
+ $(e.currentTarget).select2("destroy") |
|
50 |
+ $(e.currentTarget).val(e.removed.id) |
|
51 |
+ ) |
|
52 |
+ |
|
53 |
+ updateDropdownData = (form_data, element, data) -> |
|
54 |
+ returnedResults[form_data.attribute] = {text: 'Options', children: data} |
|
55 |
+ $(element).trigger('change') |
|
56 |
+ $(element).select2('open') |
|
57 |
+ |
|
58 |
+ $("input[role~=completable]").on 'select2-open', (e) -> |
|
59 |
+ form_data = getFormData(e.currentTarget) |
|
60 |
+ return if returnedResults[form_data.attribute] |
|
61 |
+ |
|
62 |
+ $.ajax '/agents/complete', |
|
63 |
+ type: 'POST', |
|
64 |
+ data: form_data |
|
65 |
+ success: (data) -> |
|
66 |
+ updateDropdownData(form_data, e.currentTarget, data) |
|
67 |
+ error: (data) -> |
|
68 |
+ updateDropdownData(form_data, e.currentTarget, [{id: undefined, text: 'Error loading data.'}]) |
|
69 |
+ |
|
70 |
+ $("input[type=radio][role~=form-configurable]").change (e) -> |
|
71 |
+ input = $(e.currentTarget).parents().siblings("input[data-attribute=#{$(e.currentTarget).data('attribute')}]") |
|
72 |
+ if $(e.currentTarget).val() == 'manual' |
|
73 |
+ input.removeClass('hidden') |
|
74 |
+ else |
|
75 |
+ input.val($(e.currentTarget).val()) |
|
76 |
+ input.addClass('hidden') |
@@ -1,10 +1,15 @@ |
||
1 | 1 |
$ -> |
2 |
- firstEventCount = null |
|
2 |
+ sinceId = null |
|
3 | 3 |
previousJobs = null |
4 | 4 |
|
5 | 5 |
if $(".job-indicator").length |
6 | 6 |
check = -> |
7 |
- $.getJSON "/worker_status", (json) -> |
|
7 |
+ query = |
|
8 |
+ if sinceId? |
|
9 |
+ '?since_id=' + sinceId |
|
10 |
+ else |
|
11 |
+ '' |
|
12 |
+ $.getJSON "/worker_status" + query, (json) -> |
|
8 | 13 |
for method in ['pending', 'awaiting_retry', 'recent_failures'] |
9 | 14 |
count = json[method] |
10 | 15 |
elem = $(".job-indicator[role=#{method}]") |
@@ -23,16 +28,17 @@ $ -> |
||
23 | 28 |
if elem.is(":visible") |
24 | 29 |
elem.tooltip('destroy').fadeOut() |
25 | 30 |
|
26 |
- firstEventCount = json.event_count unless firstEventCount? |
|
27 |
- if firstEventCount? && json.event_count > firstEventCount |
|
31 |
+ if sinceId? && json.event_count > 0 |
|
28 | 32 |
$("#event-indicator").tooltip('destroy'). |
29 |
- tooltip(title: "Click to reload", delay: 0, placement: "bottom", trigger: "hover"). |
|
33 |
+ tooltip(title: "Click to see the events", delay: 0, placement: "bottom", trigger: "hover"). |
|
34 |
+ find('a').attr(href: json.events_url).end(). |
|
30 | 35 |
fadeIn(). |
31 | 36 |
find(".number"). |
32 |
- text(json.event_count - firstEventCount) |
|
37 |
+ text(json.event_count) |
|
33 | 38 |
else |
34 | 39 |
$("#event-indicator").tooltip('destroy').fadeOut() |
35 | 40 |
|
41 |
+ sinceId ?= json.max_id |
|
36 | 42 |
currentJobs = [json.pending, json.awaiting_retry, json.recent_failures] |
37 | 43 |
if document.location.pathname == '/jobs' && $(".modal[aria-hidden=false]").length == 0 && previousJobs? && previousJobs.join(',') != currentJobs.join(',') |
38 | 44 |
$.get '/jobs', (data) => |
@@ -42,7 +48,3 @@ $ -> |
||
42 | 48 |
window.workerCheckTimeout = setTimeout check, 2000 |
43 | 49 |
|
44 | 50 |
check() |
45 |
- |
|
46 |
- $("#event-indicator a").on "click", (e) -> |
|
47 |
- e.preventDefault() |
|
48 |
- window.location.reload() |
@@ -18,7 +18,6 @@ class @AgentEditPage |
||
18 | 18 |
else |
19 | 19 |
$(".agent-settings").show() |
20 | 20 |
$("#agent-spinner").fadeIn() |
21 |
- $("#agent_source_ids").select2("val", {}) |
|
22 | 21 |
$(".model-errors").hide() unless firstTime |
23 | 22 |
$.getJSON "/agents/type_details", { type: type }, (json) => |
24 | 23 |
if json.can_be_scheduled |
@@ -46,11 +45,12 @@ class @AgentEditPage |
||
46 | 45 |
|
47 | 46 |
$(".description").show().html(json.description_html) if json.description_html? |
48 | 47 |
|
49 |
- $('.oauthable-form').html(json.form) if json.form? |
|
50 |
- |
|
51 | 48 |
unless firstTime |
52 |
- window.jsonEditor.json = json.options |
|
53 |
- window.jsonEditor.rebuild() |
|
49 |
+ $('.oauthable-form').html(json.oauthable) if json.oauthable? |
|
50 |
+ $('.agent-options').html(json.form_options) if json.form_options? |
|
51 |
+ window.jsonEditor = setupJsonEditor()[0] |
|
52 |
+ |
|
53 |
+ window.initializeFormCompletable() |
|
54 | 54 |
|
55 | 55 |
$("#agent-spinner").stop(true, true).fadeOut(); |
56 | 56 |
|
@@ -15,6 +15,27 @@ class @AgentShowPage |
||
15 | 15 |
$("#logs .refresh, #logs .clear").hide() |
16 | 16 |
$.get "/agents/#{agentId}/logs", (html) => |
17 | 17 |
$("#logs .logs").html html |
18 |
+ $("#logs .logs .show-log-details").each -> |
|
19 |
+ $button = $(this) |
|
20 |
+ $button.on 'click', (e) -> |
|
21 |
+ e.preventDefault() |
|
22 |
+ $("body").append """ |
|
23 |
+ <div class="modal fade" tabindex="-1" id='dynamic-modal' role="dialog" aria-labelledby="dynamic-modal-label" aria-hidden="true"> |
|
24 |
+ <div class="modal-dialog modal-lg"> |
|
25 |
+ <div class="modal-content"> |
|
26 |
+ <div class="modal-header"> |
|
27 |
+ <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button> |
|
28 |
+ <h4 class="modal-title" id="dynamic-modal-label"></h4> |
|
29 |
+ </div> |
|
30 |
+ <div class="modal-body"><pre></pre></div> |
|
31 |
+ </div> |
|
32 |
+ </div> |
|
33 |
+ </div> |
|
34 |
+ """ |
|
35 |
+ $('#dynamic-modal').find('.modal-title').text $button.data('modal-title') |
|
36 |
+ $('#dynamic-modal').find('.modal-body pre').text $button.data('modal-content') |
|
37 |
+ $('#dynamic-modal').modal('show').on 'hidden.bs.modal', -> $('#dynamic-modal').remove() |
|
38 |
+ |
|
18 | 39 |
$("#logs .spinner").stop(true, true).fadeOut -> |
19 | 40 |
$("#logs .refresh, #logs .clear").show() |
20 | 41 |
|
@@ -29,15 +29,19 @@ body { padding-top: 60px; } |
||
29 | 29 |
|
30 | 30 |
/* Rails scaffold style compatibility */ |
31 | 31 |
#error_explanation { |
32 |
- @extend .alert; |
|
33 |
- @extend .alert-error; |
|
34 |
- @extend .alert-block; |
|
32 |
+ color: #f00; |
|
33 |
+ ul { |
|
34 |
+ list-style: none; |
|
35 |
+ margin: 0 0 18px 0; |
|
36 |
+ } |
|
35 | 37 |
} |
36 | 38 |
|
37 | 39 |
.field_with_errors { |
38 |
- @extend .control-group.error; |
|
40 |
+ @extend .has-error; |
|
39 | 41 |
} |
40 | 42 |
|
43 |
+.modal-body pre { white-space: pre-wrap; } |
|
44 |
+ |
|
41 | 45 |
.select2 { |
42 | 46 |
float: none !important; |
43 | 47 |
margin-left: 0 !important; |
@@ -157,6 +161,7 @@ span.not-applicable:after { |
||
157 | 161 |
height: 16px; |
158 | 162 |
display: inline-block; |
159 | 163 |
vertical-align: inherit; |
164 |
+ cursor: pointer; |
|
160 | 165 |
|
161 | 166 |
&.refresh { |
162 | 167 |
margin: 0 10px; |
@@ -240,7 +245,22 @@ h2 .scenario, a span.label.scenario { |
||
240 | 245 |
width: 200px; |
241 | 246 |
} |
242 | 247 |
|
243 |
-.btn-auth { |
|
248 |
+$services: twitter 37signals github tumblr dropbox; |
|
249 |
+$service-colors: #55acee #8fc857 #444444 #2c4762 #007EE5; |
|
250 |
+ |
|
251 |
+@mixin services { |
|
252 |
+ @each $service in $services { |
|
253 |
+ $i: index($services, $service); |
|
254 |
+ $service-color: nth($service-colors, $i); |
|
255 |
+ |
|
256 |
+ &.service-#{$service} { |
|
257 |
+ color: #fff; |
|
258 |
+ background-color: $service-color; |
|
259 |
+ } |
|
260 |
+ } |
|
261 |
+} |
|
262 |
+ |
|
263 |
+.btn-service { |
|
244 | 264 |
position: relative; |
245 | 265 |
padding-left: 40px; |
246 | 266 |
$border-color: rgba(0,0,0,0.2); |
@@ -259,18 +279,9 @@ h2 .scenario, a span.label.scenario { |
||
259 | 279 |
border-right: 1px solid $border-color; |
260 | 280 |
} |
261 | 281 |
|
262 |
- &.btn-auth-twitter { |
|
263 |
- color: #fff; |
|
264 |
- background-color: #55acee; |
|
265 |
- } |
|
266 |
- |
|
267 |
- &.btn-auth-37signals { |
|
268 |
- color: #fff; |
|
269 |
- background-color: #8fc857; |
|
270 |
- } |
|
282 |
+ @include services; |
|
283 |
+} |
|
271 | 284 |
|
272 |
- &.btn-auth-github { |
|
273 |
- color: #fff; |
|
274 |
- background-color: #444; |
|
275 |
- } |
|
285 |
+.label-service { |
|
286 |
+ @include services; |
|
276 | 287 |
} |
@@ -20,6 +20,20 @@ |
||
20 | 20 |
} |
21 | 21 |
} |
22 | 22 |
|
23 |
+.table-striped > tbody > tr.hl { |
|
24 |
+ &:nth-child(odd) { |
|
25 |
+ > td, > th { |
|
26 |
+ background-color: #ffeecc; |
|
27 |
+ } |
|
28 |
+ } |
|
29 |
+ |
|
30 |
+ &:nth-child(even) { |
|
31 |
+ > td, > th { |
|
32 |
+ background-color: #f9e8c6; |
|
33 |
+ } |
|
34 |
+ } |
|
35 |
+} |
|
36 |
+ |
|
23 | 37 |
table.events { |
24 | 38 |
.payload { |
25 | 39 |
color: #999; |
@@ -12,11 +12,11 @@ module AgentControllerConcern |
||
12 | 12 |
end |
13 | 13 |
|
14 | 14 |
def control_action |
15 |
- options['action'].presence || 'run' |
|
15 |
+ interpolated['action'] |
|
16 | 16 |
end |
17 | 17 |
|
18 | 18 |
def validate_control_action |
19 |
- case control_action |
|
19 |
+ case options['action'] |
|
20 | 20 |
when 'run' |
21 | 21 |
control_targets.each { |target| |
22 | 22 |
if target.cannot_be_scheduled? |
@@ -24,24 +24,49 @@ module AgentControllerConcern |
||
24 | 24 |
end |
25 | 25 |
} |
26 | 26 |
when 'enable', 'disable' |
27 |
+ when nil |
|
28 |
+ errors.add(:base, "action must be specified") |
|
29 |
+ when /\{[%{]/ |
|
30 |
+ # Liquid template |
|
27 | 31 |
else |
28 | 32 |
errors.add(:base, 'invalid action') |
29 | 33 |
end |
30 | 34 |
end |
31 | 35 |
|
32 | 36 |
def control! |
33 |
- control_targets.active.each { |target| |
|
37 |
+ control_targets.each { |target| |
|
34 | 38 |
begin |
35 | 39 |
case control_action |
36 | 40 |
when 'run' |
37 |
- log "Agent run queued for '#{target.name}'" |
|
38 |
- Agent.async_check(target.id) |
|
41 |
+ case |
|
42 |
+ when target.cannot_be_scheduled? |
|
43 |
+ error "'#{target.name}' cannot run without an incoming event" |
|
44 |
+ when target.disabled? |
|
45 |
+ log "Agent run ignored for disabled Agent '#{target.name}'" |
|
46 |
+ else |
|
47 |
+ Agent.async_check(target.id) |
|
48 |
+ log "Agent run queued for '#{target.name}'" |
|
49 |
+ end |
|
39 | 50 |
when 'enable' |
40 |
- log "Enabling the Agent '#{target.name}'" |
|
41 |
- target.update!(disable: false) if target.disabled? |
|
51 |
+ case |
|
52 |
+ when target.disabled? |
|
53 |
+ target.update!(disabled: false) |
|
54 |
+ log "Agent '#{target.name}' is enabled" |
|
55 |
+ else |
|
56 |
+ log "Agent '#{target.name}' is already enabled" |
|
57 |
+ end |
|
42 | 58 |
when 'disable' |
43 |
- log "Disabling the Agent '#{target.name}'" |
|
44 |
- target.update!(disable: true) unless target.disabled? |
|
59 |
+ case |
|
60 |
+ when target.disabled? |
|
61 |
+ log "Agent '#{target.name}' is alread disabled" |
|
62 |
+ else |
|
63 |
+ target.update!(disabled: true) |
|
64 |
+ log "Agent '#{target.name}' is disabled" |
|
65 |
+ end |
|
66 |
+ when '' |
|
67 |
+ # Do nothing |
|
68 |
+ else |
|
69 |
+ error "Unsupported action '#{control_action}' ignored for '#{target.name}'" |
|
45 | 70 |
end |
46 | 71 |
rescue => e |
47 | 72 |
error "Failed to #{control_action} '#{target.name}': #{e.message}" |
@@ -0,0 +1,35 @@ |
||
1 |
+module DropboxConcern |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ included do |
|
5 |
+ include Oauthable |
|
6 |
+ valid_oauth_providers :dropbox |
|
7 |
+ gem_dependency_check { defined?(Dropbox) && Devise.omniauth_providers.include?(:dropbox) } |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ def dropbox |
|
11 |
+ Dropbox::API::Config.app_key = consumer_key |
|
12 |
+ Dropbox::API::Config.app_secret = consumer_secret |
|
13 |
+ Dropbox::API::Config.mode = 'dropbox' |
|
14 |
+ Dropbox::API::Client.new(token: oauth_token, secret: oauth_token_secret) |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ private |
|
18 |
+ |
|
19 |
+ def consumer_key |
|
20 |
+ (config = Devise.omniauth_configs[:dropbox]) && config.strategy.consumer_key |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ def consumer_secret |
|
24 |
+ (config = Devise.omniauth_configs[:dropbox]) && config.strategy.consumer_secret |
|
25 |
+ end |
|
26 |
+ |
|
27 |
+ def oauth_token |
|
28 |
+ service && service.token |
|
29 |
+ end |
|
30 |
+ |
|
31 |
+ def oauth_token_secret |
|
32 |
+ service && service.secret |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+end |
@@ -0,0 +1,66 @@ |
||
1 |
+module FormConfigurable |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ included do |
|
5 |
+ class_attribute :_form_configurable_fields |
|
6 |
+ self._form_configurable_fields = HashWithIndifferentAccess.new { |h,k| h[k] = [] } |
|
7 |
+ end |
|
8 |
+ |
|
9 |
+ delegate :form_configurable_attributes, to: :class |
|
10 |
+ delegate :form_configurable_fields, to: :class |
|
11 |
+ |
|
12 |
+ def is_form_configurable? |
|
13 |
+ true |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ def validate_option(method) |
|
17 |
+ if self.respond_to? "validate_#{method}".to_sym |
|
18 |
+ self.send("validate_#{method}".to_sym) |
|
19 |
+ else |
|
20 |
+ false |
|
21 |
+ end |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ def complete_option(method) |
|
25 |
+ if self.respond_to? "complete_#{method}".to_sym |
|
26 |
+ self.send("complete_#{method}".to_sym) |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ module ClassMethods |
|
31 |
+ def form_configurable(name, *args) |
|
32 |
+ options = args.extract_options!.reverse_merge(roles: [], type: :string) |
|
33 |
+ |
|
34 |
+ if args.all? { |arg| arg.is_a?(Symbol) } |
|
35 |
+ options.assert_valid_keys([:type, :roles, :values]) |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ if options[:type] == :array && (options[:values].blank? || !options[:values].is_a?(Array)) |
|
39 |
+ raise ArgumentError.new('When using :array as :type you need to provide the :values as an Array') |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ if options[:roles].is_a?(Symbol) |
|
43 |
+ options[:roles] = [options[:roles]] |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ if options[:type] == :array |
|
47 |
+ options[:roles] << :completable |
|
48 |
+ class_eval <<-EOF |
|
49 |
+ def complete_#{name} |
|
50 |
+ #{options[:values]}.map { |v| {text: v, id: v} } |
|
51 |
+ end |
|
52 |
+ EOF |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ _form_configurable_fields[name] = options |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+ def form_configurable_fields |
|
59 |
+ self._form_configurable_fields |
|
60 |
+ end |
|
61 |
+ |
|
62 |
+ def form_configurable_attributes |
|
63 |
+ form_configurable_fields.keys |
|
64 |
+ end |
|
65 |
+ end |
|
66 |
+end |
@@ -7,7 +7,7 @@ module TwitterConcern |
||
7 | 7 |
validate :validate_twitter_options |
8 | 8 |
valid_oauth_providers :twitter |
9 | 9 |
|
10 |
- gem_dependency_check { defined?(Twitter) && Devise.omniauth_providers.include?(:twitter) } |
|
10 |
+ gem_dependency_check { defined?(Twitter) && Devise.omniauth_providers.include?(:twitter) && ENV['TWITTER_OAUTH_KEY'].present? && ENV['TWITTER_OAUTH_SECRET'].present? } |
|
11 | 11 |
end |
12 | 12 |
|
13 | 13 |
def validate_twitter_options |
@@ -46,9 +46,9 @@ module TwitterConcern |
||
46 | 46 |
|
47 | 47 |
module ClassMethods |
48 | 48 |
def twitter_dependencies_missing |
49 |
- if defined?(Twitter) |
|
49 |
+ if ENV['TWITTER_OAUTH_KEY'].blank? || ENV['TWITTER_OAUTH_SECRET'].blank? |
|
50 | 50 |
"## Set TWITTER_OAUTH_KEY and TWITTER_OAUTH_SECRET in your environment to use Twitter Agents." |
51 |
- else |
|
51 |
+ elsif !defined?(Twitter) || !Devise.omniauth_providers.include?(:twitter) |
|
52 | 52 |
"## Include the `twitter`, `omniauth-twitter`, and `cantino-twitter-stream` gems in your Gemfile to use Twitter Agents." |
53 | 53 |
end |
54 | 54 |
end |
@@ -35,6 +35,8 @@ class AgentsController < ApplicationController |
||
35 | 35 |
|
36 | 36 |
def type_details |
37 | 37 |
@agent = Agent.build_for_type(params[:type], current_user, {}) |
38 |
+ initialize_presenter |
|
39 |
+ |
|
38 | 40 |
render :json => { |
39 | 41 |
:can_be_scheduled => @agent.can_be_scheduled?, |
40 | 42 |
:default_schedule => @agent.default_schedule, |
@@ -43,7 +45,8 @@ class AgentsController < ApplicationController |
||
43 | 45 |
:can_control_other_agents => @agent.can_control_other_agents?, |
44 | 46 |
:options => @agent.default_options, |
45 | 47 |
:description_html => @agent.html_description, |
46 |
- :form => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent }) |
|
48 |
+ :oauthable => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent }), |
|
49 |
+ :form_options => render_to_string(partial: 'options', locals: { agent: @agent }) |
|
47 | 50 |
} |
48 | 51 |
end |
49 | 52 |
|
@@ -92,6 +95,7 @@ class AgentsController < ApplicationController |
||
92 | 95 |
else |
93 | 96 |
@agent = agents.build |
94 | 97 |
end |
98 |
+ initialize_presenter |
|
95 | 99 |
|
96 | 100 |
respond_to do |format| |
97 | 101 |
format.html |
@@ -101,17 +105,18 @@ class AgentsController < ApplicationController |
||
101 | 105 |
|
102 | 106 |
def edit |
103 | 107 |
@agent = current_user.agents.find(params[:id]) |
108 |
+ initialize_presenter |
|
104 | 109 |
end |
105 | 110 |
|
106 | 111 |
def create |
107 |
- @agent = Agent.build_for_type(params[:agent].delete(:type), |
|
108 |
- current_user, |
|
109 |
- params[:agent]) |
|
112 |
+ build_agent |
|
113 |
+ |
|
110 | 114 |
respond_to do |format| |
111 | 115 |
if @agent.save |
112 | 116 |
format.html { redirect_back "'#{@agent.name}' was successfully created." } |
113 | 117 |
format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
114 | 118 |
else |
119 |
+ initialize_presenter |
|
115 | 120 |
format.html { render action: "new" } |
116 | 121 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
117 | 122 |
end |
@@ -126,6 +131,7 @@ class AgentsController < ApplicationController |
||
126 | 131 |
format.html { redirect_back "'#{@agent.name}' was successfully updated." } |
127 | 132 |
format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
128 | 133 |
else |
134 |
+ initialize_presenter |
|
129 | 135 |
format.html { render action: "edit" } |
130 | 136 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
131 | 137 |
end |
@@ -153,6 +159,22 @@ class AgentsController < ApplicationController |
||
153 | 159 |
end |
154 | 160 |
end |
155 | 161 |
|
162 |
+ def validate |
|
163 |
+ build_agent |
|
164 |
+ |
|
165 |
+ if @agent.validate_option(params[:attribute]) |
|
166 |
+ render text: 'ok' |
|
167 |
+ else |
|
168 |
+ render text: 'error', status: 403 |
|
169 |
+ end |
|
170 |
+ end |
|
171 |
+ |
|
172 |
+ def complete |
|
173 |
+ build_agent |
|
174 |
+ |
|
175 |
+ render json: @agent.complete_option(params[:attribute]) |
|
176 |
+ end |
|
177 |
+ |
|
156 | 178 |
protected |
157 | 179 |
|
158 | 180 |
# Sanitize params[:return] to prevent open redirect attacks, a common security issue. |
@@ -167,4 +189,16 @@ class AgentsController < ApplicationController |
||
167 | 189 |
|
168 | 190 |
redirect_to path, notice: message |
169 | 191 |
end |
192 |
+ |
|
193 |
+ def build_agent |
|
194 |
+ @agent = Agent.build_for_type(params[:agent].delete(:type), |
|
195 |
+ current_user, |
|
196 |
+ params[:agent]) |
|
197 |
+ end |
|
198 |
+ |
|
199 |
+ def initialize_presenter |
|
200 |
+ if @agent.present? && @agent.is_form_configurable? |
|
201 |
+ @agent = FormConfigurableAgentPresenter.new(@agent, view_context) |
|
202 |
+ end |
|
203 |
+ end |
|
170 | 204 |
end |
@@ -1,7 +1,7 @@ |
||
1 | 1 |
class ApplicationController < ActionController::Base |
2 | 2 |
protect_from_forgery |
3 | 3 |
|
4 |
- before_filter :authenticate_user! |
|
4 |
+ before_action :authenticate_user! |
|
5 | 5 |
before_action :configure_permitted_parameters, if: :devise_controller? |
6 | 6 |
|
7 | 7 |
helper :all |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
class EventsController < ApplicationController |
2 |
- before_filter :load_event, :except => :index |
|
2 |
+ before_action :load_event, except: :index |
|
3 | 3 |
|
4 | 4 |
def index |
5 | 5 |
if params[:agent_id] |
@@ -1,7 +1,7 @@ |
||
1 | 1 |
class HomeController < ApplicationController |
2 |
- skip_before_filter :authenticate_user! |
|
2 |
+ skip_before_action :authenticate_user! |
|
3 | 3 |
|
4 |
- before_filter :upgrade_warning, only: :index |
|
4 |
+ before_action :upgrade_warning, only: :index |
|
5 | 5 |
|
6 | 6 |
def index |
7 | 7 |
end |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
class JobsController < ApplicationController |
2 |
- before_filter :authenticate_admin! |
|
2 |
+ before_action :authenticate_admin! |
|
3 | 3 |
|
4 | 4 |
def index |
5 | 5 |
@jobs = Delayed::Job.order("coalesce(failed_at,'1000-01-01'), run_at asc").page(params[:page]) |
@@ -1,5 +1,5 @@ |
||
1 | 1 |
class LogsController < ApplicationController |
2 |
- before_filter :load_agent |
|
2 |
+ before_action :load_agent |
|
3 | 3 |
|
4 | 4 |
def index |
5 | 5 |
@logs = @agent.logs.all |
@@ -0,0 +1,15 @@ |
||
1 |
+class OmniauthCallbacksController < Devise::OmniauthCallbacksController |
|
2 |
+ def action_missing(name) |
|
3 |
+ case name.to_sym |
|
4 |
+ when *Devise.omniauth_providers |
|
5 |
+ service = current_user.services.initialize_or_update_via_omniauth(request.env['omniauth.auth']) |
|
6 |
+ if service && service.save |
|
7 |
+ redirect_to services_path, notice: "The service was successfully created." |
|
8 |
+ else |
|
9 |
+ redirect_to services_path, error: "Error creating the service." |
|
10 |
+ end |
|
11 |
+ else |
|
12 |
+ raise ActionController::RoutingError, 'not found' |
|
13 |
+ end |
|
14 |
+ end |
|
15 |
+end |
@@ -1,6 +1,6 @@ |
||
1 | 1 |
class ScenariosController < ApplicationController |
2 | 2 |
include SortableTable |
3 |
- skip_before_filter :authenticate_user!, :only => :export |
|
3 |
+ skip_before_action :authenticate_user!, only: :export |
|
4 | 4 |
|
5 | 5 |
def index |
6 | 6 |
set_table_sort sorts: %w[name public], default: { name: :asc } |
@@ -1,7 +1,7 @@ |
||
1 | 1 |
class ServicesController < ApplicationController |
2 | 2 |
include SortableTable |
3 | 3 |
|
4 |
- before_filter :upgrade_warning, only: :index |
|
4 |
+ before_action :upgrade_warning, only: :index |
|
5 | 5 |
|
6 | 6 |
def index |
7 | 7 |
set_table_sort sorts: %w[provider name global], default: { provider: :asc } |
@@ -33,13 +33,4 @@ class ServicesController < ApplicationController |
||
33 | 33 |
format.json { render json: @service } |
34 | 34 |
end |
35 | 35 |
end |
36 |
- |
|
37 |
- def callback |
|
38 |
- @service = current_user.services.initialize_or_update_via_omniauth(request.env['omniauth.auth']) |
|
39 |
- if @service && @service.save |
|
40 |
- redirect_to services_path, notice: "The service was successfully created." |
|
41 |
- else |
|
42 |
- redirect_to services_path, error: "Error creating the service." |
|
43 |
- end |
|
44 |
- end |
|
45 | 36 |
end |
@@ -16,7 +16,8 @@ |
||
16 | 16 |
# ["not found", 404, 'text/plain'] |
17 | 17 |
|
18 | 18 |
class WebRequestsController < ApplicationController |
19 |
- skip_before_filter :authenticate_user! |
|
19 |
+ skip_before_action :verify_authenticity_token |
|
20 |
+ skip_before_action :authenticate_user! |
|
20 | 21 |
|
21 | 22 |
def handle_request |
22 | 23 |
user = User.find_by_id(params[:user_id]) |
@@ -1,12 +1,32 @@ |
||
1 | 1 |
class WorkerStatusController < ApplicationController |
2 | 2 |
def show |
3 |
- start = Time.now.to_f |
|
4 |
- render :json => { |
|
5 |
- :pending => Delayed::Job.where("run_at <= ? AND locked_at IS NULL AND attempts = 0", Time.now).count, |
|
6 |
- :awaiting_retry => Delayed::Job.where("failed_at IS NULL AND attempts > 0").count, |
|
7 |
- :recent_failures => Delayed::Job.where("failed_at IS NOT NULL AND failed_at > ?", 5.days.ago).count, |
|
8 |
- :event_count => current_user.events.count, |
|
9 |
- :compute_time => Time.now.to_f - start |
|
3 |
+ start = Time.now |
|
4 |
+ events = current_user.events |
|
5 |
+ |
|
6 |
+ if params[:since_id].present? |
|
7 |
+ since_id = params[:since_id].to_i |
|
8 |
+ events = events.where('id > ?', since_id) |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ result = events.select('COUNT(id) AS count', 'MIN(id) AS min_id', 'MAX(id) AS max_id').reorder('min(created_at)').first |
|
12 |
+ count, min_id, max_id = result.count, result.min_id, result.max_id |
|
13 |
+ |
|
14 |
+ case max_id |
|
15 |
+ when nil |
|
16 |
+ when min_id |
|
17 |
+ events_url = events_path(hl: max_id) |
|
18 |
+ else |
|
19 |
+ events_url = events_path(hl: "#{min_id}-#{max_id}") |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ render json: { |
|
23 |
+ pending: Delayed::Job.pending.where("run_at <= ?", start).count, |
|
24 |
+ awaiting_retry: Delayed::Job.awaiting_retry.count, |
|
25 |
+ recent_failures: Delayed::Job.failed.where('failed_at > ?', 5.days.ago).count, |
|
26 |
+ event_count: count, |
|
27 |
+ max_id: max_id || 0, |
|
28 |
+ events_url: events_url, |
|
29 |
+ compute_time: Time.now - start |
|
10 | 30 |
} |
11 | 31 |
end |
12 | 32 |
end |
@@ -1,7 +1,7 @@ |
||
1 | 1 |
module AgentHelper |
2 | 2 |
def agent_show_view(agent) |
3 | 3 |
name = agent.short_type.underscore |
4 |
- if File.exists?(Rails.root.join("app", "views", "agents", "agent_views", name, "_show.html.erb")) |
|
4 |
+ if File.exist?(Rails.root.join("app", "views", "agents", "agent_views", name, "_show.html.erb")) |
|
5 | 5 |
File.join("agents", "agent_views", name, "show") |
6 | 6 |
end |
7 | 7 |
end |
@@ -1,8 +1,20 @@ |
||
1 | 1 |
module ApplicationHelper |
2 |
- def nav_link(name, path, options = {}, &block) |
|
3 |
- if glyphicon = options.delete(:glyphicon) |
|
4 |
- name = "<span class='glyphicon glyphicon-#{glyphicon}'></span> ".html_safe + name |
|
2 |
+ def icon_tag(name, options = {}) |
|
3 |
+ if dom_class = options[:class] |
|
4 |
+ dom_class = ' ' << dom_class |
|
5 | 5 |
end |
6 |
+ |
|
7 |
+ case name |
|
8 |
+ when /\Aglyphicon-/ |
|
9 |
+ "<span class='glyphicon #{name}#{dom_class}'></span>".html_safe |
|
10 |
+ when /\Afa-/ |
|
11 |
+ "<i class='fa #{name}#{dom_class}'></i>".html_safe |
|
12 |
+ else |
|
13 |
+ raise "Unrecognized icon name: #{name}" |
|
14 |
+ end |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ def nav_link(name, path, options = {}, &block) |
|
6 | 18 |
content = link_to(name, path, options) |
7 | 19 |
active = current_page?(path) |
8 | 20 |
if block |
@@ -41,12 +53,55 @@ module ApplicationHelper |
||
41 | 53 |
end |
42 | 54 |
end |
43 | 55 |
|
44 |
- def icon_for_service(service) |
|
45 |
- case service.to_sym |
|
46 |
- when :twitter, :tumblr, :github |
|
47 |
- "<i class='fa fa-#{service}'></i>".html_safe |
|
56 |
+ def omniauth_provider_icon(provider) |
|
57 |
+ case provider.to_sym |
|
58 |
+ when :twitter, :tumblr, :github, :dropbox |
|
59 |
+ icon_tag("fa-#{provider}") |
|
48 | 60 |
else |
49 |
- "<i class='fa fa-lock'></i>".html_safe |
|
61 |
+ icon_tag("fa-lock") |
|
50 | 62 |
end |
51 | 63 |
end |
64 |
+ |
|
65 |
+ def omniauth_provider_name(provider) |
|
66 |
+ t("devise.omniauth_providers.#{provider}") |
|
67 |
+ end |
|
68 |
+ |
|
69 |
+ def omniauth_button(provider) |
|
70 |
+ link_to [ |
|
71 |
+ omniauth_provider_icon(provider), |
|
72 |
+ content_tag(:span, "Authenticate with #{omniauth_provider_name(provider)}") |
|
73 |
+ ].join.html_safe, user_omniauth_authorize_path(provider), class: "btn btn-default btn-service service-#{provider}" |
|
74 |
+ end |
|
75 |
+ |
|
76 |
+ def service_label_text(service) |
|
77 |
+ "#{omniauth_provider_name(service.provider)} - #{service.name}" |
|
78 |
+ end |
|
79 |
+ |
|
80 |
+ def service_label(service) |
|
81 |
+ content_tag :span, [ |
|
82 |
+ omniauth_provider_icon(service.provider), |
|
83 |
+ service_label_text(service) |
|
84 |
+ ].join.html_safe, class: "label label-default label-service service-#{service.provider}" |
|
85 |
+ end |
|
86 |
+ |
|
87 |
+ def highlighted?(id) |
|
88 |
+ @highlighted_ranges ||= |
|
89 |
+ case value = params[:hl].presence |
|
90 |
+ when String |
|
91 |
+ value.split(/,/).flat_map { |part| |
|
92 |
+ case part |
|
93 |
+ when /\A(\d+)\z/ |
|
94 |
+ (part.to_i)..(part.to_i) |
|
95 |
+ when /\A(\d+)?-(\d+)?\z/ |
|
96 |
+ ($1 ? $1.to_i : 1)..($2 ? $2.to_i : Float::INFINITY) |
|
97 |
+ else |
|
98 |
+ [] |
|
99 |
+ end |
|
100 |
+ } |
|
101 |
+ else |
|
102 |
+ [] |
|
103 |
+ end |
|
104 |
+ |
|
105 |
+ @highlighted_ranges.any? { |range| range.cover?(id) } |
|
106 |
+ end |
|
52 | 107 |
end |
@@ -88,6 +88,10 @@ class Agent < ActiveRecord::Base |
||
88 | 88 |
# Implement me in your subclass of Agent. |
89 | 89 |
end |
90 | 90 |
|
91 |
+ def is_form_configurable? |
|
92 |
+ false |
|
93 |
+ end |
|
94 |
+ |
|
91 | 95 |
def receive_web_request(params, method, format) |
92 | 96 |
# Implement me in your subclass of Agent. |
93 | 97 |
["not implemented", 404] |
@@ -384,7 +388,7 @@ class Agent < ActiveRecord::Base |
||
384 | 388 |
agent.last_receive_at = Time.now |
385 | 389 |
agent.save! |
386 | 390 |
rescue => e |
387 |
- agent.error "Exception during receive: #{e.message} -- #{e.backtrace}" |
|
391 |
+ agent.error "Exception during receive. #{e.message}: #{e.backtrace.join("\n")}" |
|
388 | 392 |
raise |
389 | 393 |
end |
390 | 394 |
end |
@@ -422,7 +426,7 @@ class Agent < ActiveRecord::Base |
||
422 | 426 |
agent.last_check_at = Time.now |
423 | 427 |
agent.save! |
424 | 428 |
rescue => e |
425 |
- agent.error "Exception during check: #{e.message} -- #{e.backtrace}" |
|
429 |
+ agent.error "Exception during check. #{e.message}: #{e.backtrace.join("\n")}" |
|
426 | 430 |
raise |
427 | 431 |
end |
428 | 432 |
end |
@@ -32,6 +32,6 @@ class AgentLog < ActiveRecord::Base |
||
32 | 32 |
protected |
33 | 33 |
|
34 | 34 |
def truncate_message |
35 |
- self.message = message[0...2048] if message.present? |
|
35 |
+ self.message = message[0...10_000] if message.present? |
|
36 | 36 |
end |
37 | 37 |
end |
@@ -1,21 +1,16 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class BasecampAgent < Agent |
3 |
- cannot_receive_events! |
|
4 |
- |
|
3 |
+ include FormConfigurable |
|
5 | 4 |
include Oauthable |
6 | 5 |
valid_oauth_providers :'37signals' |
7 | 6 |
|
7 |
+ cannot_receive_events! |
|
8 |
+ |
|
8 | 9 |
description <<-MD |
9 | 10 |
The BasecampAgent checks a Basecamp project for new Events |
10 | 11 |
|
11 | 12 |
To be able to use this Agent you need to authenticate with 37signals in the [Services](/services) section first. |
12 | 13 |
|
13 |
- You need to provide the `project_id` of the project you want to monitor. |
|
14 |
- If you have your Basecamp project opened in your browser you can find the user_id and project_id as follows: |
|
15 |
- |
|
16 |
- `https://basecamp.com/123456/projects/` |
|
17 |
- project_id |
|
18 |
- `-explore-basecamp` |
|
19 | 14 |
MD |
20 | 15 |
|
21 | 16 |
event_description <<-MD |
@@ -50,6 +45,14 @@ module Agents |
||
50 | 45 |
} |
51 | 46 |
end |
52 | 47 |
|
48 |
+ form_configurable :project_id, roles: :completable |
|
49 |
+ |
|
50 |
+ def complete_project_id |
|
51 |
+ service.prepare_request |
|
52 |
+ response = HTTParty.get projects_url, request_options.merge(query_parameters) |
|
53 |
+ response.map { |p| {text: "#{p['name']} (#{p['id']})", id: p['id']}} |
|
54 |
+ end |
|
55 |
+ |
|
53 | 56 |
def validate_options |
54 | 57 |
errors.add(:base, "you need to specify the basecamp project id of which you want to receive events") unless options['project_id'].present? |
55 | 58 |
end |
@@ -60,8 +63,8 @@ module Agents |
||
60 | 63 |
|
61 | 64 |
def check |
62 | 65 |
service.prepare_request |
63 |
- reponse = HTTParty.get request_url, request_options.merge(query_parameters) |
|
64 |
- events = JSON.parse(reponse.body) |
|
66 |
+ response = HTTParty.get events_url, request_options.merge(query_parameters) |
|
67 |
+ events = JSON.parse(response.body) |
|
65 | 68 |
if !memory[:last_event].nil? |
66 | 69 |
events.each do |event| |
67 | 70 |
create_event :payload => event |
@@ -72,8 +75,16 @@ module Agents |
||
72 | 75 |
end |
73 | 76 |
|
74 | 77 |
private |
75 |
- def request_url |
|
76 |
- "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json" |
|
78 |
+ def base_url |
|
79 |
+ "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/" |
|
80 |
+ end |
|
81 |
+ |
|
82 |
+ def events_url |
|
83 |
+ base_url + "projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json" |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ def projects_url |
|
87 |
+ base_url + "projects.json" |
|
77 | 88 |
end |
78 | 89 |
|
79 | 90 |
def request_options |
@@ -0,0 +1,47 @@ |
||
1 |
+module Agents |
|
2 |
+ class CommanderAgent < Agent |
|
3 |
+ include AgentControllerConcern |
|
4 |
+ |
|
5 |
+ cannot_create_events! |
|
6 |
+ |
|
7 |
+ description <<-MD |
|
8 |
+ This agent is triggered by schedule or an incoming event and commands other agents ("targets") to run, disable or enable themselves. |
|
9 |
+ |
|
10 |
+ # Action types |
|
11 |
+ |
|
12 |
+ Set `action` to one of the action types below: |
|
13 |
+ |
|
14 |
+ * `run`: Target Agents are run when this agent is triggered. |
|
15 |
+ |
|
16 |
+ * `disable`: Target Agents are disabled (if not) when this agent is triggered. |
|
17 |
+ |
|
18 |
+ * `enable`: Target Agents are enabled (if not) when this agent is triggered. |
|
19 |
+ |
|
20 |
+ Here's a tip: you can use Liquid templating to dynamically determine the action type. For example: |
|
21 |
+ |
|
22 |
+ - To create a CommanderAgent that receives an event from WeatherAgent every morning to kick an agent flow that is only useful in a nice weather, try this: `{% if conditions contains 'Sunny' or conditions contains 'Cloudy' %}run{% endif %}` |
|
23 |
+ |
|
24 |
+ - Likewise, if you have a scheduled agent flow specially crafted for rainy days, try this: `{% if conditions contains 'Rain' %}enable{% else %}disabled{% endif %}` |
|
25 |
+ |
|
26 |
+ # Targets |
|
27 |
+ |
|
28 |
+ Select Agents that you want to control from this CommanderAgent. |
|
29 |
+ MD |
|
30 |
+ |
|
31 |
+ def working? |
|
32 |
+ true |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ def check! |
|
36 |
+ control! |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ def receive(incoming_events) |
|
40 |
+ incoming_events.each do |event| |
|
41 |
+ interpolate_with(event) do |
|
42 |
+ control! |
|
43 |
+ end |
|
44 |
+ end |
|
45 |
+ end |
|
46 |
+ end |
|
47 |
+end |
@@ -0,0 +1,58 @@ |
||
1 |
+module Agents |
|
2 |
+ class DropboxFileUrlAgent < Agent |
|
3 |
+ include DropboxConcern |
|
4 |
+ |
|
5 |
+ cannot_be_scheduled! |
|
6 |
+ |
|
7 |
+ description <<-MD |
|
8 |
+ #{'## Include the `dropbox-api` and `omniauth-dropbox` gems in your `Gemfile` and set `DROPBOX_OAUTH_KEY` and `DROPBOX_OAUTH_SECRET` in your environment to use Dropbox Agents.' if dependencies_missing?} |
|
9 |
+ The _DropboxFileUrlAgent_ is used to work with Dropbox. It takes a file path (or multiple files paths) and emits |
|
10 |
+ events with [temporary links](https://www.dropbox.com/developers/core/docs#media). |
|
11 |
+ |
|
12 |
+ The incoming event payload needs to have a `paths` key, with a comma-separated list of files you want the URL for. For example: |
|
13 |
+ |
|
14 |
+ { |
|
15 |
+ "paths": "first/path, second/path" |
|
16 |
+ } |
|
17 |
+ |
|
18 |
+ __TIP__: You can use the _Event Formatting Agent_ to format events before they come in. Here's an example configuration for formatting an event coming out of a _Dropbox Watch Agent_: |
|
19 |
+ |
|
20 |
+ { |
|
21 |
+ "instructions": { |
|
22 |
+ "paths": "{{ added | map: 'path' | join: ',' }}" |
|
23 |
+ }, |
|
24 |
+ "matchers": [], |
|
25 |
+ "mode": "clean" |
|
26 |
+ } |
|
27 |
+ |
|
28 |
+ An example of usage would be to watch a specific Dropbox directory (with the _DropboxWatchAgent_) and get the URLs for the added or updated files. You could then, for example, send emails with those links. |
|
29 |
+ |
|
30 |
+ MD |
|
31 |
+ |
|
32 |
+ event_description <<-MD |
|
33 |
+ The event payload will contain the following fields: |
|
34 |
+ |
|
35 |
+ { |
|
36 |
+ "url": "https://dl.dropboxusercontent.com/1/view/abcdefghijk/example", |
|
37 |
+ "expires": "Fri, 16 Sep 2011 01:01:25 +0000" |
|
38 |
+ } |
|
39 |
+ MD |
|
40 |
+ |
|
41 |
+ def working? |
|
42 |
+ !recent_error_logs? |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ def receive(events) |
|
46 |
+ events.map { |e| e.payload['paths'].split(',').map(&:strip) } |
|
47 |
+ .flatten.each { |path| create_event payload: url_for(path) } |
|
48 |
+ end |
|
49 |
+ |
|
50 |
+ private |
|
51 |
+ |
|
52 |
+ def url_for(path) |
|
53 |
+ dropbox.find(path).direct_url |
|
54 |
+ end |
|
55 |
+ |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+end |
@@ -0,0 +1,114 @@ |
||
1 |
+module Agents |
|
2 |
+ class DropboxWatchAgent < Agent |
|
3 |
+ include DropboxConcern |
|
4 |
+ |
|
5 |
+ cannot_receive_events! |
|
6 |
+ default_schedule "every_1m" |
|
7 |
+ |
|
8 |
+ description <<-MD |
|
9 |
+ #{'## Include the `dropbox-api` and `omniauth-dropbox` gems in your `Gemfile` and set `DROPBOX_OAUTH_KEY` and `DROPBOX_OAUTH_SECRET` in your environment to use Dropbox Agents.' if dependencies_missing?} |
|
10 |
+ The _DropboxWatchAgent_ watches the given `dir_to_watch` and emits events with the detected changes. |
|
11 |
+ MD |
|
12 |
+ |
|
13 |
+ event_description <<-MD |
|
14 |
+ The event payload will contain the following fields: |
|
15 |
+ |
|
16 |
+ { |
|
17 |
+ "added": [ { |
|
18 |
+ "path": "/path/to/added/file", |
|
19 |
+ "rev": "1526952fd5", |
|
20 |
+ "modified": "Fri, 10 Oct 2014 19:00:43 +0000" |
|
21 |
+ } ], |
|
22 |
+ "removed": [ ... ], |
|
23 |
+ "updated": [ ... ] |
|
24 |
+ } |
|
25 |
+ MD |
|
26 |
+ |
|
27 |
+ def default_options |
|
28 |
+ { |
|
29 |
+ 'dir_to_watch' => '/', |
|
30 |
+ 'expected_update_period_in_days' => 1 |
|
31 |
+ } |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ def validate_options |
|
35 |
+ errors.add(:base, 'The `dir_to_watch` property is required.') unless options['dir_to_watch'].present? |
|
36 |
+ errors.add(:base, 'Invalid `expected_update_period_in_days` format.') unless options['expected_update_period_in_days'].present? && is_positive_integer?(options['expected_update_period_in_days']) |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ def working? |
|
40 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !received_event_without_error? |
|
41 |
+ end |
|
42 |
+ |
|
43 |
+ def check |
|
44 |
+ current_contents = ls(interpolated['dir_to_watch']) |
|
45 |
+ diff = DropboxDirDiff.new(previous_contents, current_contents) |
|
46 |
+ create_event(payload: diff.to_hash) unless previous_contents.nil? || diff.empty? |
|
47 |
+ |
|
48 |
+ remember(current_contents) |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ private |
|
52 |
+ |
|
53 |
+ def is_positive_integer?(value) |
|
54 |
+ Integer(value) >= 0 |
|
55 |
+ rescue |
|
56 |
+ false |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ def ls(dir_to_watch) |
|
60 |
+ dropbox.ls(dir_to_watch).map { |entry| slice_json(entry, 'path', 'rev', 'modified') } |
|
61 |
+ end |
|
62 |
+ |
|
63 |
+ def slice_json(json, *keys) |
|
64 |
+ keys.each_with_object({}){|key, hash| hash[key.to_s] = json[key.to_s]} |
|
65 |
+ end |
|
66 |
+ |
|
67 |
+ def previous_contents |
|
68 |
+ self.memory['contents'] |
|
69 |
+ end |
|
70 |
+ |
|
71 |
+ def remember(contents) |
|
72 |
+ self.memory['contents'] = contents |
|
73 |
+ end |
|
74 |
+ |
|
75 |
+ # == Auxiliary classes == |
|
76 |
+ |
|
77 |
+ class DropboxDirDiff |
|
78 |
+ def initialize(previous, current) |
|
79 |
+ @previous, @current = [previous || [], current || []] |
|
80 |
+ end |
|
81 |
+ |
|
82 |
+ def empty? |
|
83 |
+ (@previous == @current) |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ def to_hash |
|
87 |
+ calculate_diff |
|
88 |
+ { added: @added, removed: @removed, updated: @updated } |
|
89 |
+ end |
|
90 |
+ |
|
91 |
+ private |
|
92 |
+ |
|
93 |
+ def calculate_diff |
|
94 |
+ @updated = @current.select do |current_entry| |
|
95 |
+ previous_entry = find_by_path(@previous, current_entry['path']) |
|
96 |
+ (current_entry != previous_entry) && !previous_entry.nil? |
|
97 |
+ end |
|
98 |
+ |
|
99 |
+ updated_entries = @updated + @previous.select do |previous_entry| |
|
100 |
+ find_by_path(@updated, previous_entry['path']) |
|
101 |
+ end |
|
102 |
+ |
|
103 |
+ @added = @current - @previous - updated_entries |
|
104 |
+ @removed = @previous - @current - updated_entries |
|
105 |
+ end |
|
106 |
+ |
|
107 |
+ def find_by_path(array, path) |
|
108 |
+ array.find { |entry| entry['path'] == path } |
|
109 |
+ end |
|
110 |
+ end |
|
111 |
+ |
|
112 |
+ end |
|
113 |
+ |
|
114 |
+end |
@@ -1,5 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class HipchatAgent < Agent |
3 |
+ include FormConfigurable |
|
4 |
+ |
|
3 | 5 |
cannot_be_scheduled! |
4 | 6 |
cannot_create_events! |
5 | 7 |
|
@@ -15,8 +17,10 @@ module Agents |
||
15 | 17 |
|
16 | 18 |
Change the `room_name` to the name of the room you want to send notifications to. |
17 | 19 |
|
18 |
- You can provide a `username` and a `message`. When sending a HTML formatted message change `format` to "html". |
|
19 |
- If you want your message to notify the room members change `notify` to "true". |
|
20 |
+ You can provide a `username` and a `message`. If you want to use mentions change `format` to "text" ([details](https://www.hipchat.com/docs/api/method/rooms/message)). |
|
21 |
+ |
|
22 |
+ If you want your message to notify the room members change `notify` to "True". |
|
23 |
+ |
|
20 | 24 |
Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random") |
21 | 25 |
|
22 | 26 |
Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating. |
@@ -30,9 +34,29 @@ module Agents |
||
30 | 34 |
'message' => "Hello from Huginn!", |
31 | 35 |
'notify' => false, |
32 | 36 |
'color' => 'yellow', |
37 |
+ 'format' => 'html' |
|
33 | 38 |
} |
34 | 39 |
end |
35 | 40 |
|
41 |
+ form_configurable :auth_token, roles: :validatable |
|
42 |
+ form_configurable :room_name, roles: :completable |
|
43 |
+ form_configurable :username |
|
44 |
+ form_configurable :message, type: :text |
|
45 |
+ form_configurable :notify, type: :boolean |
|
46 |
+ form_configurable :color, type: :array, values: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] |
|
47 |
+ form_configurable :format, type: :array, values: ['html', 'text'] |
|
48 |
+ |
|
49 |
+ def validate_auth_token |
|
50 |
+ client.rooms |
|
51 |
+ true |
|
52 |
+ rescue HipChat::UnknownResponseCode |
|
53 |
+ return false |
|
54 |
+ end |
|
55 |
+ |
|
56 |
+ def complete_room_name |
|
57 |
+ client.rooms.collect { |room| {text: room.name, id: room.name} } |
|
58 |
+ end |
|
59 |
+ |
|
36 | 60 |
def validate_options |
37 | 61 |
errors.add(:base, "you need to specify a hipchat auth_token or provide a credential named hipchat_auth_token") unless options['auth_token'].present? || credential('hipchat_auth_token').present? |
38 | 62 |
errors.add(:base, "you need to specify a room_name or a room_name_path") if options['room_name'].blank? && options['room_name_path'].blank? |
@@ -45,12 +69,17 @@ module Agents |
||
45 | 69 |
def receive(incoming_events) |
46 | 70 |
incoming_events.each do |event| |
47 | 71 |
mo = interpolated(event) |
48 |
- client[mo[:room_name]].send(mo[:username][0..14], mo[:message], :notify => boolify(mo[:notify]), :color => mo[:color]) |
|
72 |
+ client[mo[:room_name]].send(mo[:username][0..14], mo[:message], |
|
73 |
+ notify: boolify(mo[:notify]), |
|
74 |
+ color: mo[:color], |
|
75 |
+ message_format: mo[:format].presence || 'html' |
|
76 |
+ ) |
|
49 | 77 |
end |
50 | 78 |
end |
51 | 79 |
|
80 |
+ private |
|
52 | 81 |
def client |
53 |
- @client ||= HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token')) |
|
82 |
+ @client ||= HipChat::Client.new(interpolated[:auth_token].presence || credential('hipchat_auth_token')) |
|
54 | 83 |
end |
55 | 84 |
end |
56 | 85 |
end |
@@ -453,6 +453,14 @@ module Agents |
||
453 | 453 |
ret |
454 | 454 |
end |
455 | 455 |
|
456 |
+ def fetch(*args) |
|
457 |
+ super || [] |
|
458 |
+ end |
|
459 |
+ |
|
460 |
+ def uid_fetch(*args) |
|
461 |
+ super || [] |
|
462 |
+ end |
|
463 |
+ |
|
456 | 464 |
def uid_fetch_mails(set) |
457 | 465 |
uid_fetch(set, 'RFC822.HEADER').map { |data| |
458 | 466 |
Message.new(self, data, folder: @folder, uidvalidity: @uidvalidity) |
@@ -110,31 +110,44 @@ module Agents |
||
110 | 110 |
incoming_events.each do |event| |
111 | 111 |
c.publish(interpolated(event)['topic'], event) |
112 | 112 |
end |
113 |
- |
|
114 |
- c.disconnect |
|
115 | 113 |
end |
116 | 114 |
end |
117 | 115 |
|
118 | 116 |
|
119 | 117 |
def check |
120 |
- mqtt_client.connect do |c| |
|
121 |
- |
|
122 |
- Timeout::timeout((interpolated['max_read_time'].presence || 15).to_i) { |
|
123 |
- c.get(interpolated['topic']) do |topic, message| |
|
124 |
- |
|
125 |
- # A lot of services generate JSON. Try that first |
|
126 |
- payload = JSON.parse(message) rescue message |
|
118 |
+ last_message = memory['last_message'] |
|
127 | 119 |
|
128 |
- create_event :payload => { |
|
129 |
- 'topic' => topic, |
|
130 |
- 'message' => payload, |
|
131 |
- 'time' => Time.now.to_i |
|
132 |
- } |
|
133 |
- end |
|
134 |
- } rescue TimeoutError |
|
135 |
- |
|
136 |
- c.disconnect |
|
120 |
+ mqtt_client.connect do |c| |
|
121 |
+ begin |
|
122 |
+ Timeout.timeout((interpolated['max_read_time'].presence || 15).to_i) { |
|
123 |
+ c.get_packet(interpolated['topic']) do |packet| |
|
124 |
+ topic, payload = message = [packet.topic, packet.payload] |
|
125 |
+ |
|
126 |
+ # Ignore a message if it is previously received |
|
127 |
+ next if (packet.retain || packet.duplicate) && message == last_message |
|
128 |
+ |
|
129 |
+ last_message = message |
|
130 |
+ |
|
131 |
+ # A lot of services generate JSON, so try that. |
|
132 |
+ begin |
|
133 |
+ payload = JSON.parse(payload) |
|
134 |
+ rescue |
|
135 |
+ end |
|
136 |
+ |
|
137 |
+ create_event payload: { |
|
138 |
+ 'topic' => topic, |
|
139 |
+ 'message' => payload, |
|
140 |
+ 'time' => Time.now.to_i |
|
141 |
+ } |
|
142 |
+ end |
|
143 |
+ } |
|
144 |
+ rescue Timeout::Error |
|
145 |
+ end |
|
137 | 146 |
end |
147 |
+ |
|
148 |
+ # Remember the last original (non-retain, non-duplicate) message |
|
149 |
+ self.memory['last_message'] = last_message |
|
150 |
+ save! |
|
138 | 151 |
end |
139 | 152 |
|
140 | 153 |
end |
@@ -41,6 +41,7 @@ module Agents |
||
41 | 41 |
"id": "829f845279611d7925146725317b868d", |
42 | 42 |
"date_published": "2014-09-11 01:30:00 -0700", |
43 | 43 |
"last_updated": "Thu, 11 Sep 2014 01:30:00 -0700", |
44 |
+ "url": "http://example.com/...", |
|
44 | 45 |
"urls": [ "http://example.com/..." ], |
45 | 46 |
"description": "Some description", |
46 | 47 |
"content": "Some content", |
@@ -75,16 +76,17 @@ module Agents |
||
75 | 76 |
entry_id = get_entry_id(entry) |
76 | 77 |
if check_and_track(entry_id) |
77 | 78 |
created_event_count += 1 |
78 |
- create_event(:payload => { |
|
79 |
- :id => entry_id, |
|
80 |
- :date_published => entry.date_published, |
|
81 |
- :last_updated => entry.last_updated, |
|
82 |
- :urls => entry.urls, |
|
83 |
- :description => entry.description, |
|
84 |
- :content => entry.content, |
|
85 |
- :title => entry.title, |
|
86 |
- :authors => entry.authors, |
|
87 |
- :categories => entry.categories |
|
79 |
+ create_event(payload: { |
|
80 |
+ id: entry_id, |
|
81 |
+ date_published: entry.date_published, |
|
82 |
+ last_updated: entry.last_updated, |
|
83 |
+ url: entry.url, |
|
84 |
+ urls: entry.urls, |
|
85 |
+ description: entry.description, |
|
86 |
+ content: entry.content, |
|
87 |
+ title: entry.title, |
|
88 |
+ authors: entry.authors, |
|
89 |
+ categories: entry.categories |
|
88 | 90 |
}) |
89 | 91 |
end |
90 | 92 |
end |
@@ -19,7 +19,7 @@ module Agents |
||
19 | 19 |
|
20 | 20 |
Set `action` to one of the action types below: |
21 | 21 |
|
22 |
- * `run`: This is the default. Target Agents are run at intervals. |
|
22 |
+ * `run`: Target Agents are run at intervals, except for those disabled. |
|
23 | 23 |
|
24 | 24 |
* `disable`: Target Agents are disabled (if not) at intervals. |
25 | 25 |
|
@@ -6,7 +6,8 @@ module Agents |
||
6 | 6 |
|
7 | 7 |
description do |
8 | 8 |
<<-MD |
9 |
- The UserLocationAgent creates events based on WebHook POSTS that contain a `latitude` and `longitude`. You can use the POSTLocation iOS app to post your location. |
|
9 |
+ The UserLocationAgent creates events based on WebHook POSTS that contain a `latitude` and `longitude`. You can use the [POSTLocation](https://github.com/cantino/post_location) or [PostGPS](https://github.com/chriseidhof/PostGPS) iOS app to post your location. |
|
10 |
+ |
|
10 | 11 |
|
11 | 12 |
Your POST path will be `https://#{ENV['DOMAIN']}/users/#{user.id}/update_location/:secret` where `:secret` is specified in your options. |
12 | 13 |
MD |
@@ -209,7 +209,7 @@ module Agents |
||
209 | 209 |
} |
210 | 210 |
end |
211 | 211 |
rescue => e |
212 |
- error e.message |
|
212 |
+ error "Error when fetching url: #{e.message}\n#{e.backtrace.join("\n")}" |
|
213 | 213 |
end |
214 | 214 |
|
215 | 215 |
def receive(incoming_events) |
@@ -111,7 +111,7 @@ class ScenarioImport |
||
111 | 111 |
|
112 | 112 |
def parse_file |
113 | 113 |
if data.blank? && file.present? |
114 |
- self.data = file.read |
|
114 |
+ self.data = file.read.force_encoding(Encoding::UTF_8) |
|
115 | 115 |
end |
116 | 116 |
end |
117 | 117 |
|
@@ -59,12 +59,10 @@ class Service < ActiveRecord::Base |
||
59 | 59 |
|
60 | 60 |
def self.provider_specific_options(omniauth) |
61 | 61 |
case omniauth['provider'].to_sym |
62 |
- when :twitter, :github |
|
63 |
- { name: omniauth['info']['nickname'] } |
|
64 | 62 |
when :'37signals' |
65 | 63 |
{ user_id: omniauth['extra']['accounts'][0]['id'], name: omniauth['info']['name'] } |
66 | 64 |
else |
67 |
- { name: omniauth['info']['nickname'] } |
|
65 |
+ { name: omniauth['info']['nickname'] || omniauth['info']['name'] } |
|
68 | 66 |
end |
69 | 67 |
end |
70 | 68 |
|
@@ -0,0 +1,44 @@ |
||
1 |
+require 'delegate' |
|
2 |
+ |
|
3 |
+class Decorator < SimpleDelegator |
|
4 |
+ def class |
|
5 |
+ __getobj__.class |
|
6 |
+ end |
|
7 |
+end |
|
8 |
+ |
|
9 |
+class FormConfigurableAgentPresenter < Decorator |
|
10 |
+ def initialize(agent, view) |
|
11 |
+ @agent = agent |
|
12 |
+ @view = view |
|
13 |
+ super(agent) |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ def option_field_for(attribute) |
|
17 |
+ data = @agent.form_configurable_fields[attribute] |
|
18 |
+ value = @agent.options[attribute.to_s] || @agent.default_options[attribute.to_s] |
|
19 |
+ html_options = {role: (data[:roles] + ['form-configurable']).join(' '), data: {attribute: attribute}} |
|
20 |
+ |
|
21 |
+ case data[:type] |
|
22 |
+ when :text |
|
23 |
+ @view.text_area_tag "agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3) |
|
24 |
+ when :boolean |
|
25 |
+ @view.content_tag 'div' do |
|
26 |
+ @view.concat(@view.content_tag('label', class: 'radio-inline') do |
|
27 |
+ @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'true', @agent.send(:boolify, value) == true, html_options |
|
28 |
+ @view.concat "True" |
|
29 |
+ end) |
|
30 |
+ @view.concat(@view.content_tag('label', class: 'radio-inline') do |
|
31 |
+ @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'false', @agent.send(:boolify, value) == false, html_options |
|
32 |
+ @view.concat "False" |
|
33 |
+ end) |
|
34 |
+ @view.concat(@view.content_tag('label', class: 'radio-inline') do |
|
35 |
+ @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'manual', @agent.send(:boolify, value) == nil, html_options |
|
36 |
+ @view.concat "Manual Input" |
|
37 |
+ end) |
|
38 |
+ @view.concat(@view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => "form-control #{@agent.send(:boolify, value) != nil ? 'hidden' : ''}")) |
|
39 |
+ end |
|
40 |
+ when :array, :string |
|
41 |
+ @view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => 'form-control') |
|
42 |
+ end |
|
43 |
+ end |
|
44 |
+end |
@@ -1,30 +1,30 @@ |
||
1 | 1 |
<ul class="dropdown-menu" role="menu"> |
2 | 2 |
<% if agent.can_be_scheduled? %> |
3 | 3 |
<li> |
4 |
- <%= link_to '<span class="color-success glyphicon glyphicon-refresh"></span> Run'.html_safe, run_agent_path(agent, :return => returnTo), method: :post, :tabindex => "-1" %> |
|
4 |
+ <%= link_to icon_tag('glyphicon-refresh', class: 'color-success') + ' Run', run_agent_path(agent, return: returnTo), method: :post, tabindex: "-1" %> |
|
5 | 5 |
</li> |
6 | 6 |
<% end %> |
7 | 7 |
|
8 | 8 |
<li> |
9 |
- <%= link_to '<span class="glyphicon glyphicon-eye-open"></span> Show'.html_safe, agent_path(agent) %> |
|
9 |
+ <%= link_to icon_tag('glyphicon-eye-open') + ' Show'.html_safe, agent_path(agent) %> |
|
10 | 10 |
</li> |
11 | 11 |
|
12 | 12 |
<li class="divider"></li> |
13 | 13 |
|
14 | 14 |
<li> |
15 |
- <%= link_to '<span class="glyphicon glyphicon-pencil"></span> Edit agent'.html_safe, edit_agent_path(agent) %> |
|
15 |
+ <%= link_to icon_tag('glyphicon-pencil') + ' Edit agent'.html_safe, edit_agent_path(agent) %> |
|
16 | 16 |
</li> |
17 | 17 |
|
18 | 18 |
<li> |
19 |
- <%= link_to '<span class="glyphicon glyphicon-plus"></span> Clone agent'.html_safe, new_agent_path(id: agent), :tabindex => "-1" %> |
|
19 |
+ <%= link_to icon_tag('fa-copy') + ' Clone agent'.html_safe, new_agent_path(id: agent), tabindex: "-1" %> |
|
20 | 20 |
</li> |
21 | 21 |
|
22 | 22 |
<li> |
23 | 23 |
<%= link_to '#', 'data-toggle' => 'modal', 'data-target' => "#confirm-agent#{agent.id}" do %> |
24 | 24 |
<% if agent.disabled? %> |
25 |
- <i class="glyphicon glyphicon-play"></i> Enable agent |
|
25 |
+ <%= icon_tag('glyphicon-play') %> Enable agent |
|
26 | 26 |
<% else %> |
27 |
- <i class="glyphicon glyphicon-pause"></i> Disable agent |
|
27 |
+ <%= icon_tag('glyphicon-pause') %> Disable agent |
|
28 | 28 |
<% end %> |
29 | 29 |
<% end %> |
30 | 30 |
</li> |
@@ -34,7 +34,7 @@ |
||
34 | 34 |
|
35 | 35 |
<% agent.scenarios.each do |scenario| %> |
36 | 36 |
<li> |
37 |
- <%= link_to "<span class='color-warning glyphicon glyphicon-remove-circle'></span> Remove from #{scenario_label(scenario)}".html_safe, leave_scenario_agent_path(agent, :scenario_id => scenario.to_param, :return => returnTo), method: :put, :tabindex => "-1" %> |
|
37 |
+ <%= link_to icon_tag('glyphicon-remove-circle', class: 'color-warning') + " Remove from #{scenario_label(scenario)}".html_safe, leave_scenario_agent_path(agent, scenario_id: scenario.to_param, return: returnTo), method: :put, tabindex: "-1" %> |
|
38 | 38 |
</li> |
39 | 39 |
<% end %> |
40 | 40 |
<% end %> |
@@ -43,12 +43,12 @@ |
||
43 | 43 |
|
44 | 44 |
<% if agent.can_create_events? && agent.events.count > 0 %> |
45 | 45 |
<li> |
46 |
- <%= link_to '<span class="color-danger glyphicon glyphicon-trash"></span> Delete all events'.html_safe, remove_events_agent_path(agent, :return => returnTo), method: :delete, data: {confirm: 'Are you sure you want to delete ALL emitted events for this Agent?'}, :tabindex => "-1" %> |
|
46 |
+ <%= link_to icon_tag('glyphicon-trash', class: 'color-danger') + ' Delete all events', remove_events_agent_path(agent, return: returnTo), method: :delete, data: {confirm: 'Are you sure you want to delete ALL emitted events for this Agent?'}, tabindex: "-1" %> |
|
47 | 47 |
</li> |
48 | 48 |
<% end %> |
49 | 49 |
|
50 | 50 |
<li> |
51 |
- <%= link_to '<span class="color-danger glyphicon glyphicon-remove"></span> Delete agent'.html_safe, agent_path(agent, :return => returnTo), method: :delete, data: { confirm: 'Are you sure that you want to permanently delete this Agent?' }, :tabindex => "-1" %> |
|
51 |
+ <%= link_to icon_tag('glyphicon-remove', class: 'color-danger') + ' Delete agent', agent_path(agent, return: returnTo), method: :delete, data: { confirm: 'Are you sure that you want to permanently delete this Agent?' }, tabindex: "-1" %> |
|
52 | 52 |
</li> |
53 | 53 |
</ul> |
54 | 54 |
|
@@ -16,8 +16,7 @@ |
||
16 | 16 |
<div class="col-md-6"> |
17 | 17 |
<div class="row"> |
18 | 18 |
|
19 |
- <!-- Form controls width restricted --> |
|
20 |
- <div class="col-md-8"> |
|
19 |
+ <div class="col-md-12"> |
|
21 | 20 |
<% if @agent.new_record? %> |
22 | 21 |
<div class="form-group type-select"> |
23 | 22 |
<%= f.label :type %> |
@@ -27,7 +26,7 @@ |
||
27 | 26 |
</div> |
28 | 27 |
|
29 | 28 |
<div class="agent-settings"> |
30 |
- <div class="col-md-8"> |
|
29 |
+ <div class="col-md-12"> |
|
31 | 30 |
<div class="form-group"> |
32 | 31 |
<%= f.label :name %> |
33 | 32 |
<%= f.text_field :name, :class => 'form-control' %> |
@@ -105,19 +104,8 @@ |
||
105 | 104 |
|
106 | 105 |
</div> |
107 | 106 |
|
108 |
- <!-- Form controls full width --> |
|
109 |
- <div class="col-md-12"> |
|
110 |
- <div class="form-group"> |
|
111 |
- <%= f.label :options %> |
|
112 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span> |
|
113 |
- <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor"> |
|
114 |
- <%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %> |
|
115 |
- </textarea> |
|
116 |
- </div> |
|
117 |
- |
|
118 |
- <div class="form-group"> |
|
119 |
- <%= f.submit "Save", :class => "btn btn-primary" %> |
|
120 |
- </div> |
|
107 |
+ <div class="col-md-12 agent-options"> |
|
108 |
+ <%= render partial: 'options', locals: { agent: @agent } %> |
|
121 | 109 |
</div> |
122 | 110 |
</div> |
123 | 111 |
</div> |
@@ -1,6 +1,6 @@ |
||
1 | 1 |
<% if agent.try(:oauthable?) %> |
2 | 2 |
<div class="form-group type-select"> |
3 | 3 |
<%= label_tag :service %> |
4 |
- <%= select_tag 'agent[service_id]', options_for_select(agent.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, agent.service_id), class: 'form-control' %> |
|
4 |
+ <%= select_tag 'agent[service_id]', options_for_select(agent.valid_services_for(current_user).collect { |s| [service_label_text(s), s.id] }, agent.service_id), class: 'form-control' %> |
|
5 | 5 |
</div> |
6 | 6 |
<% end %> |
@@ -0,0 +1,27 @@ |
||
1 |
+<% if agent.is_form_configurable? %> |
|
2 |
+ <fieldset> |
|
3 |
+ <% if agent.persisted? %> |
|
4 |
+ <%= hidden_field_tag 'agent[type]', @agent.type %> |
|
5 |
+ <% end %> |
|
6 |
+ <legend>Options</legend> |
|
7 |
+ <% agent.form_configurable_attributes.each do |attribute| %> |
|
8 |
+ <div class="form-group"> |
|
9 |
+ <%= label_tag attribute %> |
|
10 |
+ <%= agent.option_field_for(attribute) %> |
|
11 |
+ <span class="glyphicon glyphicon-ok form-control-feedback hidden"></span> |
|
12 |
+ <span class="glyphicon glyphicon-remove form-control-feedback hidden"></span> |
|
13 |
+ </div> |
|
14 |
+ <% end %> |
|
15 |
+ </fieldset> |
|
16 |
+<% else %> |
|
17 |
+ <div class="form-group"> |
|
18 |
+ <%= label_tag :options %> |
|
19 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span> |
|
20 |
+ <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor"> |
|
21 |
+ <%= Utils.jsonify((agent.new_record? && agent.options == {}) ? agent.default_options : agent.options) %> |
|
22 |
+ </textarea> |
|
23 |
+ </div> |
|
24 |
+<% end %> |
|
25 |
+<div class="form-group"> |
|
26 |
+ <%= submit_tag "Save", :class => "btn btn-primary" %> |
|
27 |
+</div> |
@@ -9,17 +9,18 @@ |
||
9 | 9 |
</div> |
10 | 10 |
</div> |
11 | 11 |
</div> |
12 |
- |
|
13 |
- <%= render 'form' %> |
|
12 |
+ <div id="agent-form"> |
|
13 |
+ <%= render 'form' %> |
|
14 |
+ </div> |
|
14 | 15 |
|
15 | 16 |
<hr> |
16 | 17 |
|
17 | 18 |
<div class="row"> |
18 | 19 |
<div class="col-md-12"> |
19 | 20 |
<div class="btn-group"> |
20 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path, class: "btn btn-default" %> |
|
21 |
- <%= link_to '<span class="glyphicon glyphicon-asterisk"></span> Show'.html_safe, agent_path(@agent), class: "btn btn-default" %> |
|
21 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back', agents_path, class: "btn btn-default" %> |
|
22 |
+ <%= link_to icon_tag('glyphicon-asterisk') + ' Show', agent_path(@agent), class: "btn btn-default" %> |
|
22 | 23 |
</div> |
23 | 24 |
</div> |
24 | 25 |
</div> |
25 |
-</div> |
|
26 |
+</div> |
@@ -10,9 +10,9 @@ |
||
10 | 10 |
<br/> |
11 | 11 |
|
12 | 12 |
<div class="btn-group"> |
13 |
- <%= link_to '<span class="glyphicon glyphicon-plus"></span> New Agent'.html_safe, new_agent_path, class: "btn btn-default" %> |
|
14 |
- <%= link_to '<span class="glyphicon glyphicon-refresh"></span> Run event propagation'.html_safe, propagate_agents_path, method: 'post', class: "btn btn-default" %> |
|
15 |
- <%= link_to '<span class="glyphicon glyphicon-random"></span> View diagram'.html_safe, diagram_path, class: "btn btn-default" %> |
|
13 |
+ <%= link_to icon_tag('glyphicon-plus') + ' New Agent', new_agent_path, class: "btn btn-default" %> |
|
14 |
+ <%= link_to icon_tag('glyphicon-refresh') + ' Run event propagation', propagate_agents_path, method: 'post', class: "btn btn-default" %> |
|
15 |
+ <%= link_to icon_tag('glyphicon-random') + ' View diagram', diagram_path, class: "btn btn-default" %> |
|
16 | 16 |
</div> |
17 | 17 |
</div> |
18 | 18 |
</div> |
@@ -8,15 +8,17 @@ |
||
8 | 8 |
</h2> |
9 | 9 |
</div> |
10 | 10 |
|
11 |
- <%= render 'form' %> |
|
11 |
+ <div id="agent-form"> |
|
12 |
+ <%= render 'form' %> |
|
13 |
+ </div> |
|
12 | 14 |
|
13 | 15 |
<hr> |
14 | 16 |
|
15 | 17 |
<div class="row"> |
16 | 18 |
<div class="col-md-12"> |
17 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path, class: "btn btn-default" %> |
|
19 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, agents_path, class: "btn btn-default" %> |
|
18 | 20 |
</div> |
19 | 21 |
</div> |
20 | 22 |
</div> |
21 | 23 |
</div> |
22 |
-</div> |
|
24 |
+</div> |
@@ -2,7 +2,7 @@ |
||
2 | 2 |
<div class='row'> |
3 | 3 |
<div class='col-md-2'> |
4 | 4 |
<ul class="nav nav-pills nav-stacked" id="show-tabs"> |
5 |
- <li><%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path %></li> |
|
5 |
+ <li><%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, agents_path %></li> |
|
6 | 6 |
|
7 | 7 |
<% if agent_show_view(@agent).present? %> |
8 | 8 |
<li class='active'><a href="#summary" data-toggle="tab"><span class='glyphicon glyphicon-picture'></span> Summary</a></li> |
@@ -15,7 +15,7 @@ |
||
15 | 15 |
<li><a href="#logs" data-toggle="tab" data-agent-id="<%= @agent.id %>" class='<%= @agent.recent_error_logs? ? 'recent-errors' : '' %>'><span class='glyphicon glyphicon-list-alt'></span> Logs</a></li> |
16 | 16 |
|
17 | 17 |
<% if @agent.can_create_events? && @agent.events.count > 0 %> |
18 |
- <li><%= link_to '<span class="glyphicon glyphicon-random"></span> Events'.html_safe, agent_events_path(@agent) %></li> |
|
18 |
+ <li><%= link_to icon_tag('glyphicon-random') + ' Events'.html_safe, agent_events_path(@agent) %></li> |
|
19 | 19 |
<% else %> |
20 | 20 |
<li class='disabled'><a><span class='glyphicon glyphicon-random'></span> Events</a></li> |
21 | 21 |
<% end %> |
@@ -107,6 +107,13 @@ |
||
107 | 107 |
</p> |
108 | 108 |
<% end %> |
109 | 109 |
|
110 |
+ <% if @agent.try(:oauthable?) %> |
|
111 |
+ <p> |
|
112 |
+ <b>Service:</b> |
|
113 |
+ <%= service_label(@agent.service) %> |
|
114 |
+ </p> |
|
115 |
+ <% end %> |
|
116 |
+ |
|
110 | 117 |
<% if @agent.can_receive_events? %> |
111 | 118 |
<p> |
112 | 119 |
<b>Event sources:</b> |
@@ -4,18 +4,18 @@ |
||
4 | 4 |
|
5 | 5 |
<h2>Resend confirmation instructions</h2> |
6 | 6 |
|
7 |
- <%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %> |
|
7 |
+ <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'form-horizontal' }) do |f| %> |
|
8 | 8 |
<%= devise_error_messages! %> |
9 | 9 |
|
10 | 10 |
<div class="form-group"> |
11 | 11 |
<%= f.label :email %> |
12 |
- <%= f.email_field :email, :class => 'form-control' %> |
|
12 |
+ <%= f.email_field :email, autofocus: true, class: 'form-control' %> |
|
13 | 13 |
</div> |
14 | 14 |
|
15 |
- <%= f.submit "Resend confirmation instructions", :class => "btn btn-primary" %> |
|
15 |
+ <%= f.submit "Resend confirmation instructions", class: "btn btn-primary" %> |
|
16 | 16 |
<% end %> |
17 | 17 |
|
18 | 18 |
<%= render "devise/shared/links" %> |
19 | 19 |
</div> |
20 | 20 |
</div> |
21 |
-</div> |
|
21 |
+</div> |
@@ -1,5 +1,5 @@ |
||
1 |
-<p>Welcome <%= @resource.email %>!</p> |
|
1 |
+<p>Welcome <%= @email %>!</p> |
|
2 | 2 |
|
3 | 3 |
<p>You can confirm your account email through the link below:</p> |
4 | 4 |
|
5 |
-<p><%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %></p> |
|
5 |
+<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p> |
@@ -1,8 +1,8 @@ |
||
1 | 1 |
<p>Hello <%= @resource.email %>!</p> |
2 | 2 |
|
3 |
-<p>Someone has requested a link to change your password, and you can do this through the link below.</p> |
|
3 |
+<p>Someone has requested a link to change your password. You can do this through the link below.</p> |
|
4 | 4 |
|
5 |
-<p><%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) %></p> |
|
5 |
+<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p> |
|
6 | 6 |
|
7 | 7 |
<p>If you didn't request this, please ignore this email.</p> |
8 | 8 |
<p>Your password won't change until you access the link above and create a new one.</p> |
@@ -1,7 +1,7 @@ |
||
1 | 1 |
<p>Hello <%= @resource.email %>!</p> |
2 | 2 |
|
3 |
-<p>Your account has been locked due to an excessive amount of unsuccessful sign in attempts.</p> |
|
3 |
+<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p> |
|
4 | 4 |
|
5 | 5 |
<p>Click the link below to unlock your account:</p> |
6 | 6 |
|
7 |
-<p><%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @resource.unlock_token) %></p> |
|
7 |
+<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p> |
@@ -4,26 +4,26 @@ |
||
4 | 4 |
<div class='well'> |
5 | 5 |
<h2>Change your password</h2> |
6 | 6 |
|
7 |
- <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put, :class => 'form-horizontal' }) do |f| %> |
|
7 |
+ <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'form-horizontal' }) do |f| %> |
|
8 | 8 |
<%= devise_error_messages! %> |
9 | 9 |
<%= f.hidden_field :reset_password_token %> |
10 | 10 |
|
11 | 11 |
<div class="control-group"> |
12 |
- <%= f.label :password, "New password", :class => 'control-label' %> |
|
12 |
+ <%= f.label :password, "New password", class: 'control-label' %> |
|
13 | 13 |
<div class="controls"> |
14 |
- <%= f.password_field :password, :class => 'span4' %> |
|
14 |
+ <%= f.password_field :password, autofocus: true, autocomplete: "off", class: 'span4' %> |
|
15 | 15 |
</div> |
16 | 16 |
</div> |
17 | 17 |
|
18 | 18 |
<div class="control-group"> |
19 |
- <%= f.label :password_confirmation, "Confirm new password", :class => 'control-label' %> |
|
19 |
+ <%= f.label :password_confirmation, "Confirm new password" %> |
|
20 | 20 |
<div class="controls"> |
21 |
- <%= f.password_field :password_confirmation, :class => 'span4' %> |
|
21 |
+ <%= f.password_field :password_confirmation, autocomplete: "off", class: 'span4' %> |
|
22 | 22 |
</div> |
23 | 23 |
</div> |
24 | 24 |
|
25 | 25 |
<div class='form-actions'> |
26 |
- <%= f.submit "Change my password", :class => "btn btn-primary" %> |
|
26 |
+ <%= f.submit "Change my password", class: "btn btn-primary" %> |
|
27 | 27 |
</div> |
28 | 28 |
<% end %> |
29 | 29 |
|
@@ -31,4 +31,4 @@ |
||
31 | 31 |
</div> |
32 | 32 |
</div> |
33 | 33 |
</div> |
34 |
-</div> |
|
34 |
+</div> |
@@ -3,19 +3,19 @@ |
||
3 | 3 |
<div class='well'> |
4 | 4 |
<h2>Forgot your password?</h2> |
5 | 5 |
|
6 |
- <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %> |
|
6 |
+ <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'form-horizontal' }) do |f| %> |
|
7 | 7 |
<%= devise_error_messages! %> |
8 | 8 |
|
9 | 9 |
<div class="form-group"> |
10 | 10 |
<%= f.label :login, :class => 'col-md-2 col-md-offset-2 control-label' %> |
11 | 11 |
<div class="col-md-6"> |
12 |
- <%= f.text_field :login, :class => 'form-control' %> |
|
12 |
+ <%= f.text_field :login, autofocus: true, :class => 'form-control' %> |
|
13 | 13 |
</div> |
14 | 14 |
</div> |
15 | 15 |
|
16 | 16 |
<div class="form-group"> |
17 | 17 |
<div class="col-md-offset-4 col-md-10"> |
18 |
- <%= f.submit "Send me reset password instructions", :class => "btn btn-primary" %> |
|
18 |
+ <%= f.submit "Send me reset password instructions", class: "btn btn-primary" %> |
|
19 | 19 |
</div> |
20 | 20 |
</div> |
21 | 21 |
<% end %> |
@@ -25,4 +25,4 @@ |
||
25 | 25 |
<%= render "devise/shared/links" %> |
26 | 26 |
</div> |
27 | 27 |
</div> |
28 |
-</div> |
|
28 |
+</div> |
@@ -5,66 +5,70 @@ |
||
5 | 5 |
|
6 | 6 |
<h2>Edit <%= resource_name.to_s.humanize %></h2> |
7 | 7 |
|
8 |
- <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put, :class => 'form-horizontal' }) do |f| %> |
|
8 |
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'form-horizontal' }) do |f| %> |
|
9 | 9 |
<%= devise_error_messages! %> |
10 | 10 |
|
11 | 11 |
<div class="form-group"> |
12 |
- <%= f.label :email, :class => 'col-md-4 control-label' %> |
|
12 |
+ <%= f.label :email, class: 'col-md-4 control-label' %> |
|
13 | 13 |
<div class="col-md-6"> |
14 |
- <%= f.email_field :email, :class => 'form-control' %> |
|
14 |
+ <%= f.email_field :email, autofocus: true, class: 'form-control' %> |
|
15 | 15 |
</div> |
16 | 16 |
</div> |
17 | 17 |
|
18 |
+ <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> |
|
19 |
+ <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div> |
|
20 |
+ <% end %> |
|
21 |
+ |
|
18 | 22 |
<div class="form-group"> |
19 |
- <%= f.label :username, :class => 'col-md-4 control-label' %> |
|
23 |
+ <%= f.label :username, class: 'col-md-4 control-label' %> |
|
20 | 24 |
<div class="col-md-6"> |
21 |
- <%= f.text_field :username, :class => 'form-control' %> |
|
25 |
+ <%= f.text_field :username, class: 'form-control' %> |
|
22 | 26 |
</div> |
23 | 27 |
</div> |
24 | 28 |
|
25 | 29 |
<div class="form-group"> |
26 |
- <%= f.label :current_password, :class => 'col-md-4 control-label' %> |
|
30 |
+ <%= f.label :current_password, class: 'col-md-4 control-label' %> |
|
27 | 31 |
<div class="col-md-6"> |
28 |
- <%= f.password_field :current_password, :class => 'form-control' %> |
|
32 |
+ <%= f.password_field :current_password, class: 'form-control' %> |
|
29 | 33 |
<span class='help-inline'>We need your current password to confirm your changes.</span> |
30 | 34 |
</div> |
31 | 35 |
</div> |
32 |
- |
|
36 |
+ |
|
33 | 37 |
<div class="form-group"> |
34 | 38 |
<div class="col-md-offset-4 col-md-10"> |
35 |
- <%= f.submit "Update", :class => "btn btn-primary" %> |
|
39 |
+ <%= f.submit "Update", class: "btn btn-primary" %> |
|
36 | 40 |
</div> |
37 | 41 |
</div> |
38 | 42 |
<% end %> |
39 | 43 |
|
40 | 44 |
<h3>Change password</h3> |
41 |
- <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put, :class => 'form-horizontal' }) do |f| %> |
|
45 |
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: 'form-horizontal' }) do |f| %> |
|
42 | 46 |
<%= devise_error_messages! %> |
43 | 47 |
<div class="form-group"> |
44 |
- <%= f.label :current_password, :class => 'col-md-4 control-label' %> |
|
48 |
+ <%= f.label :current_password, class: 'col-md-4 control-label' %> |
|
45 | 49 |
<div class="col-md-6"> |
46 |
- <%= f.password_field :current_password, :class => 'form-control' %> |
|
50 |
+ <%= f.password_field :current_password, class: 'form-control' %> |
|
47 | 51 |
<span class='help-inline'>We need your current password to confirm your changes.</span> |
48 | 52 |
</div> |
49 | 53 |
</div> |
50 | 54 |
|
51 | 55 |
<div class="form-group"> |
52 |
- <%= f.label :password, :class => 'col-md-4 control-label' %> |
|
56 |
+ <%= f.label :password, class: 'col-md-4 control-label' %> |
|
53 | 57 |
<div class="col-md-6"> |
54 |
- <%= f.password_field :password, :autocomplete => "off", :class => 'form-control' %> |
|
58 |
+ <%= f.password_field :password, autocomplete: "off", class: 'form-control' %> |
|
55 | 59 |
</div> |
56 | 60 |
</div> |
57 | 61 |
|
58 | 62 |
<div class="form-group"> |
59 |
- <%= f.label :password_confirmation, :class => 'col-md-4 control-label' %> |
|
63 |
+ <%= f.label :password_confirmation, class: 'col-md-4 control-label' %> |
|
60 | 64 |
<div class="col-md-6"> |
61 |
- <%= f.password_field :password_confirmation, :class => 'form-control' %> |
|
65 |
+ <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %> |
|
62 | 66 |
</div> |
63 | 67 |
</div> |
64 | 68 |
|
65 | 69 |
<div class="form-group"> |
66 | 70 |
<div class="col-md-offset-4 col-md-10"> |
67 |
- <%= f.submit "Update", :class => "btn btn-primary" %> |
|
71 |
+ <%= f.submit "Update", class: "btn btn-primary" %> |
|
68 | 72 |
</div> |
69 | 73 |
</div> |
70 | 74 |
|
@@ -74,7 +78,7 @@ |
||
74 | 78 |
|
75 | 79 |
<h3>Cancel my account</h3> |
76 | 80 |
|
77 |
- <p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), :data => { :confirm => "Are you sure?" }, :method => :delete %>.</p> |
|
81 |
+ <p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %></p> |
|
78 | 82 |
|
79 | 83 |
<%= link_to "Back", :back %> |
80 | 84 |
</div> |
@@ -3,9 +3,9 @@ |
||
3 | 3 |
<div class='col-md-8 col-md-offset-2'> |
4 | 4 |
<div class='well'> |
5 | 5 |
|
6 |
- <h2>Sign Up</h2> |
|
6 |
+ <h2>Sign up</h2> |
|
7 | 7 |
|
8 |
- <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :class => 'form-horizontal' }) do |f| %> |
|
8 |
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'form-horizontal' }) do |f| %> |
|
9 | 9 |
<%= devise_error_messages! %> |
10 | 10 |
<% if ENV['ON_HEROKU'] && User.count.zero? %> |
11 | 11 |
<div class="heroku-instructions"> |
@@ -31,44 +31,45 @@ bin/setup_heroku |
||
31 | 31 |
<% end %> |
32 | 32 |
|
33 | 33 |
<div class="form-group"> |
34 |
- <%= f.label :invitation_code, :class => 'col-md-4 control-label' %> |
|
34 |
+ <%= f.label :invitation_code, class: 'col-md-4 control-label' %> |
|
35 | 35 |
<div class="col-md-6"> |
36 |
- <%= f.text_field :invitation_code, :class => 'form-control' %> |
|
36 |
+ <%= f.text_field :invitation_code, class: 'form-control' %> |
|
37 | 37 |
<span class="help-inline">We are not yet open to the public. If you have an invitation code, please enter it here.</span> |
38 | 38 |
</div> |
39 | 39 |
</div> |
40 | 40 |
|
41 | 41 |
<div class="form-group"> |
42 |
- <%= f.label :email, :class => 'col-md-4 control-label' %> |
|
42 |
+ <%= f.label :email, class: 'col-md-4 control-label' %> |
|
43 | 43 |
<div class="col-md-6"> |
44 |
- <%= f.email_field :email, :class => 'form-control' %> |
|
44 |
+ <%= f.email_field :email, autofocus: true, class: 'form-control' %> |
|
45 | 45 |
</div> |
46 | 46 |
</div> |
47 | 47 |
|
48 | 48 |
<div class="form-group"> |
49 |
- <%= f.label :username, :class => 'col-md-4 control-label' %> |
|
49 |
+ <%= f.label :username, class: 'col-md-4 control-label' %> |
|
50 | 50 |
<div class="col-md-6"> |
51 |
- <%= f.text_field :username, :class => 'form-control' %> |
|
51 |
+ <%= f.text_field :username, class: 'form-control' %> |
|
52 | 52 |
</div> |
53 | 53 |
</div> |
54 | 54 |
|
55 | 55 |
<div class="form-group"> |
56 |
- <%= f.label :password, :class => 'col-md-4 control-label' %> |
|
56 |
+ <%= f.label :password, class: 'col-md-4 control-label' %> |
|
57 | 57 |
<div class="col-md-6"> |
58 |
- <%= f.password_field :password, :autocomplete => "off", :class => 'form-control' %> |
|
58 |
+ <%= f.password_field :password, autocomplete: "off", class: 'form-control' %> |
|
59 |
+ <% if @validatable %><span class="help-inline"><%= @minimum_password_length %> characters minimum.</span><% end %> |
|
59 | 60 |
</div> |
60 | 61 |
</div> |
61 | 62 |
|
62 | 63 |
<div class="form-group"> |
63 |
- <%= f.label :password_confirmation, :class => 'col-md-4 control-label' %> |
|
64 |
+ <%= f.label :password_confirmation, class: 'col-md-4 control-label' %> |
|
64 | 65 |
<div class="col-md-6"> |
65 |
- <%= f.password_field :password_confirmation, :class => 'form-control' %> |
|
66 |
+ <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %> |
|
66 | 67 |
</div> |
67 | 68 |
</div> |
68 | 69 |
|
69 | 70 |
<div class="form-group"> |
70 | 71 |
<div class="col-md-offset-4 col-md-10"> |
71 |
- <%= f.submit "Sign Up", :class => "btn btn-primary" %> |
|
72 |
+ <%= f.submit "Sign up", class: "btn btn-primary" %> |
|
72 | 73 |
</div> |
73 | 74 |
</div> |
74 | 75 |
|
@@ -2,29 +2,29 @@ |
||
2 | 2 |
<div class='col-md-6 col-md-offset-3'> |
3 | 3 |
<div class='well'> |
4 | 4 |
|
5 |
- <h2>Sign in</h2> |
|
5 |
+ <h2>Log in</h2> |
|
6 | 6 |
|
7 |
- <%= form_for(resource, :as => resource_name, :url => session_path(resource_name), :html => { :class => 'form-horizontal', :role => 'form'}) do |f| %> |
|
7 |
+ <%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'form-horizontal', role: 'form'}) do |f| %> |
|
8 | 8 |
<div class="form-group"> |
9 |
- <%= f.label :login, :class => 'col-md-2 col-md-offset-2 control-label' %> |
|
9 |
+ <%= f.label :login, class: 'col-md-2 col-md-offset-2 control-label' %> |
|
10 | 10 |
<div class="col-md-6"> |
11 |
- <%= f.text_field :login, :class => 'form-control' %> |
|
11 |
+ <%= f.text_field :login, autofocus: true, class: 'form-control' %> |
|
12 | 12 |
</div> |
13 | 13 |
</div> |
14 | 14 |
|
15 | 15 |
<div class="form-group"> |
16 |
- <%= f.label :password, :class => 'col-md-2 col-md-offset-2 control-label' %> |
|
16 |
+ <%= f.label :password, class: 'col-md-2 col-md-offset-2 control-label' %> |
|
17 | 17 |
<div class="col-md-6"> |
18 |
- <%= f.password_field :password, :class => 'form-control' %> |
|
18 |
+ <%= f.password_field :password, class: 'form-control' %> |
|
19 | 19 |
</div> |
20 | 20 |
</div> |
21 | 21 |
|
22 | 22 |
<div class="form-group"> |
23 | 23 |
<div class="col-md-offset-4 col-md-10"> |
24 |
- <% if devise_mapping.rememberable? %> |
|
24 |
+ <% if devise_mapping.rememberable? -%> |
|
25 | 25 |
<div class="checkbox"> |
26 | 26 |
<label> |
27 |
- <%= f.check_box :remember_me %> Remember me |
|
27 |
+ <%= f.check_box :remember_me %> <%= f.label :remember_me %> |
|
28 | 28 |
</label> |
29 | 29 |
</div> |
30 | 30 |
<% end -%> |
@@ -33,7 +33,7 @@ |
||
33 | 33 |
|
34 | 34 |
<div class="form-group"> |
35 | 35 |
<div class="col-md-offset-4 col-md-10"> |
36 |
- <%= f.submit "Sign in", :class => "btn btn-default" %> |
|
36 |
+ <%= f.submit "Log in", class: "btn btn-default" %> |
|
37 | 37 |
</div> |
38 | 38 |
</div> |
39 | 39 |
<% end %> |
@@ -43,4 +43,4 @@ |
||
43 | 43 |
<%= render "devise/shared/links" %> |
44 | 44 |
</div> |
45 | 45 |
</div> |
46 |
-</div> |
|
46 |
+</div> |
@@ -1,12 +1,12 @@ |
||
1 | 1 |
<%- if controller_name != 'sessions' %> |
2 |
- <%= link_to "Sign in", new_session_path(resource_name) %><br /> |
|
2 |
+ <%= link_to "Log in", new_session_path(resource_name) %><br /> |
|
3 | 3 |
<% end -%> |
4 | 4 |
|
5 | 5 |
<%- if devise_mapping.registerable? && controller_name != 'registrations' %> |
6 | 6 |
<%= link_to "Sign up", new_registration_path(resource_name) %><br /> |
7 | 7 |
<% end -%> |
8 | 8 |
|
9 |
-<%- if devise_mapping.recoverable? && controller_name != 'passwords' %> |
|
9 |
+<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> |
|
10 | 10 |
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br /> |
11 | 11 |
<% end -%> |
12 | 12 |
|
@@ -17,9 +17,3 @@ |
||
17 | 17 |
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> |
18 | 18 |
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br /> |
19 | 19 |
<% end -%> |
20 |
- |
|
21 |
-<%- if devise_mapping.omniauthable? %> |
|
22 |
- <%- resource_class.omniauth_providers.each do |provider| %> |
|
23 |
- <%= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %><br /> |
|
24 |
- <% end -%> |
|
25 |
-<% end -%> |
@@ -3,19 +3,19 @@ |
||
3 | 3 |
<div class='well'> |
4 | 4 |
<h2>Resend unlock instructions</h2> |
5 | 5 |
|
6 |
- <%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post, :class => 'form-horizontal' }) do |f| %> |
|
6 |
+ <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'form-horizontal' }) do |f| %> |
|
7 | 7 |
<%= devise_error_messages! %> |
8 | 8 |
|
9 | 9 |
<div class="form-group"> |
10 |
- <%= f.label :email, :class => 'col-md-2 col-md-offset-2 control-label' %> |
|
10 |
+ <%= f.label :email, class: 'col-md-2 col-md-offset-2 control-label' %> |
|
11 | 11 |
<div class="col-md-6"> |
12 |
- <%= f.text_field :email, :class => 'form-control' %> |
|
12 |
+ <%= f.email_field :email, autofocus: true, class: 'form-control' %> |
|
13 | 13 |
</div> |
14 | 14 |
</div> |
15 | 15 |
|
16 | 16 |
<div class="form-group"> |
17 | 17 |
<div class="col-md-offset-4 col-md-10"> |
18 |
- <%= f.submit "Resend unlock instructions", :class => "btn btn-primary" %> |
|
18 |
+ <%= f.submit "Resend unlock instructions", class: "btn btn-primary" %> |
|
19 | 19 |
</div> |
20 | 20 |
</div> |
21 | 21 |
<% end %> |
@@ -25,4 +25,4 @@ |
||
25 | 25 |
<%= render "devise/shared/links" %> |
26 | 26 |
</div> |
27 | 27 |
</div> |
28 |
-</div> |
|
28 |
+</div> |
@@ -9,7 +9,7 @@ |
||
9 | 9 |
<h2>Agent Event Flow</h2> |
10 | 10 |
</div> |
11 | 11 |
<div class="btn-group"> |
12 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, (params[:scenario_id] ? scenario_path(params[:scenario_id]) : agents_path), class: "btn btn-default" %> |
|
12 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, (params[:scenario_id] ? scenario_path(params[:scenario_id]) : agents_path), class: "btn btn-default" %> |
|
13 | 13 |
</div> |
14 | 14 |
|
15 | 15 |
<div class='digraph'> |
@@ -18,7 +18,7 @@ |
||
18 | 18 |
|
19 | 19 |
<% @events.each do |event| %> |
20 | 20 |
<% next unless event.agent %> |
21 |
- <tr> |
|
21 |
+ <%= content_tag :tr, class: (highlighted?(event.id) ? 'hl' : nil) do %> |
|
22 | 22 |
<td><%= link_to event.agent.name, agent_path(event.agent) %></td> |
23 | 23 |
<td title='<%= event.created_at %>'><%= time_ago_in_words event.created_at %> ago</td> |
24 | 24 |
<td class='payload'><%= truncate event.payload.to_json, :length => 90, :omission => "" %></td> |
@@ -29,19 +29,19 @@ |
||
29 | 29 |
<%= link_to 'Delete', event_path(event), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default" %> |
30 | 30 |
</div> |
31 | 31 |
</td> |
32 |
- </tr> |
|
32 |
+ <% end %> |
|
33 | 33 |
<% end %> |
34 | 34 |
</table> |
35 | 35 |
</div> |
36 | 36 |
|
37 |
- <%= paginate @events, :theme => 'twitter-bootstrap-3' %> |
|
37 |
+ <%= paginate @events, params: params.slice(:hl), theme: 'twitter-bootstrap-3' %> |
|
38 | 38 |
|
39 | 39 |
<br /> |
40 | 40 |
|
41 | 41 |
<% if @agent %> |
42 | 42 |
<div class="btn-group"> |
43 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, agents_path, class: "btn btn-default" %> |
|
44 |
- <%= link_to '<span class="glyphicon glyphicon-random""></span> See all events'.html_safe, events_path, class: "btn btn-default" %> |
|
43 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, agents_path, class: "btn btn-default" %> |
|
44 |
+ <%= link_to icon_tag('glyphicon-random') + ' See all events'.html_safe, events_path, class: "btn btn-default" %> |
|
45 | 45 |
</div> |
46 | 46 |
<% end %> |
47 | 47 |
</div> |
@@ -46,7 +46,7 @@ |
||
46 | 46 |
|
47 | 47 |
<br /> |
48 | 48 |
<div class="btn-group"> |
49 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, events_path, class: "btn btn-default" %> |
|
49 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, events_path, class: "btn btn-default" %> |
|
50 | 50 |
</div> |
51 | 51 |
</div> |
52 | 52 |
</div> |
@@ -14,9 +14,9 @@ |
||
14 | 14 |
<ul class='nav navbar-nav'> |
15 | 15 |
<%= nav_link "Agents", agents_path do %> |
16 | 16 |
<ul class='dropdown-menu' role='menu'> |
17 |
- <%= nav_link "New Agent", new_agent_path, glyphicon: "plus" %> |
|
18 |
- <%= nav_link "Run event propagation", propagate_agents_path, method: 'post', glyphicon: "refresh" %> |
|
19 |
- <%= nav_link "View Diagram", diagram_path, glyphicon: 'random' %> |
|
17 |
+ <%= nav_link icon_tag('glyphicon-plus') + " New Agent", new_agent_path %> |
|
18 |
+ <%= nav_link icon_tag('glyphicon-refresh') + " Run event propagation", propagate_agents_path, method: 'post' %> |
|
19 |
+ <%= nav_link icon_tag('glyphicon-random') + " View Diagram", diagram_path %> |
|
20 | 20 |
</ul> |
21 | 21 |
<% end %> |
22 | 22 |
<%= nav_link "Scenarios", scenarios_path %> |
@@ -37,22 +37,22 @@ |
||
37 | 37 |
|
38 | 38 |
<li class='job-indicator' role='pending'> |
39 | 39 |
<%= link_to current_user.admin? ? jobs_path : '#', class: 'visible-lg' do %> |
40 |
- <span class="badge"><span class="glyphicon glyphicon-refresh icon-white"></span> <span class='number'>0</span></span> |
|
40 |
+ <span class="badge"><%= icon_tag('glyphicon-refresh', class: 'icon-white') %> <span class='number'>0</span></span> |
|
41 | 41 |
<% end %> |
42 | 42 |
</li> |
43 | 43 |
<li class='job-indicator' role='awaiting_retry'> |
44 | 44 |
<%= link_to current_user.admin? ? jobs_path : '#', class: 'visible-lg' do %> |
45 |
- <span class="badge"><span class="glyphicon glyphicon-question-sign icon-yellow"></span> <span class='number'>0</span></span> |
|
45 |
+ <span class="badge"><%= icon_tag('glyphicon-question-sign', class: 'icon-yellow') %> <span class='number'>0</span></span> |
|
46 | 46 |
<% end %> |
47 | 47 |
</li> |
48 | 48 |
<li class='job-indicator' role='recent_failures'> |
49 | 49 |
<%= link_to current_user.admin? ? jobs_path : '#', class: 'hidden-sm hidden-xs' do %> |
50 |
- <span class="badge"><span class="glyphicon glyphicon-exclamation-sign icon-white"></span> <span class='number'>0</span></span> |
|
50 |
+ <span class="badge"><%= icon_tag('glyphicon-exclamation-sign', class: 'icon-white') %> <span class='number'>0</span></span> |
|
51 | 51 |
<% end %> |
52 | 52 |
</li> |
53 | 53 |
<li id='event-indicator'> |
54 | 54 |
<a href="#" class='hidden-sm hidden-xs'> |
55 |
- <span class="badge"><span class="glyphicon glyphicon-random icon-white"></span> <span class='number'>0</span> new events</span> |
|
55 |
+ <span class="badge"><%= icon_tag('glyphicon-random', class: 'icon-white') %> <span class='number'>0</span> new events</span> |
|
56 | 56 |
</a> |
57 | 57 |
</li> |
58 | 58 |
<% end %> |
@@ -3,7 +3,7 @@ |
||
3 | 3 |
<tr> |
4 | 4 |
<th>Message</th> |
5 | 5 |
<th>When</th> |
6 |
- <th></th> |
|
6 |
+ <th width="200px"></th> |
|
7 | 7 |
</tr> |
8 | 8 |
|
9 | 9 |
<% @logs.each do |log| %> |
@@ -24,6 +24,8 @@ |
||
24 | 24 |
<% else %> |
25 | 25 |
<%= link_to 'Event Out', '#', class: "btn btn-default disabled" %> |
26 | 26 |
<% end %> |
27 |
+ |
|
28 |
+ <%= link_to 'Details', '#', :class => "btn btn-default show-log-details", :data => { :'modal-title' => log.level >= 4 ? 'Error' : 'Info', :'modal-content' => log.message } %> |
|
27 | 29 |
</div> |
28 | 30 |
</td> |
29 | 31 |
</tr> |
@@ -26,7 +26,7 @@ |
||
26 | 26 |
|
27 | 27 |
<div class="row"> |
28 | 28 |
<div class="col-md-12"> |
29 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
29 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
30 | 30 |
</div> |
31 | 31 |
</div> |
32 |
-</div> |
|
32 |
+</div> |
@@ -13,9 +13,9 @@ |
||
13 | 13 |
|
14 | 14 |
<div class="row"> |
15 | 15 |
<div class="col-md-12"> |
16 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
16 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
17 | 17 |
</div> |
18 | 18 |
</div> |
19 | 19 |
</div> |
20 | 20 |
</div> |
21 |
-</div> |
|
21 |
+</div> |
@@ -43,8 +43,8 @@ |
||
43 | 43 |
<br/> |
44 | 44 |
|
45 | 45 |
<div class="btn-group"> |
46 |
- <%= link_to '<span class="glyphicon glyphicon-plus"></span> New Scenario'.html_safe, new_scenario_path, class: "btn btn-default" %> |
|
47 |
- <%= link_to '<span class="glyphicon glyphicon-plus"></span> Import Scenario'.html_safe, new_scenario_imports_path, class: "btn btn-default" %> |
|
46 |
+ <%= link_to icon_tag('glyphicon-plus') + ' New Scenario'.html_safe, new_scenario_path, class: "btn btn-default" %> |
|
47 |
+ <%= link_to icon_tag('glyphicon-cloud-upload') + ' Import Scenario'.html_safe, new_scenario_imports_path, class: "btn btn-default" %> |
|
48 | 48 |
</div> |
49 | 49 |
</div> |
50 | 50 |
</div> |
@@ -13,9 +13,9 @@ |
||
13 | 13 |
|
14 | 14 |
<div class="row"> |
15 | 15 |
<div class="col-md-12"> |
16 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
16 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
17 | 17 |
</div> |
18 | 18 |
</div> |
19 | 19 |
</div> |
20 | 20 |
</div> |
21 |
-</div> |
|
21 |
+</div> |
@@ -25,7 +25,7 @@ |
||
25 | 25 |
|
26 | 26 |
<div class="row"> |
27 | 27 |
<div class="col-md-12"> |
28 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenario_path(@scenario), class: "btn btn-default" %> |
|
28 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, scenario_path(@scenario), class: "btn btn-default" %> |
|
29 | 29 |
</div> |
30 | 30 |
</div> |
31 | 31 |
</div> |
@@ -15,14 +15,14 @@ |
||
15 | 15 |
<br/> |
16 | 16 |
|
17 | 17 |
<div class="btn-group"> |
18 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
|
19 |
- <%= link_to '<span class="glyphicon glyphicon-random"></span> View Diagram'.html_safe, scenario_diagram_path(@scenario), class: "btn btn-default" %> |
|
20 |
- <%= link_to '<span class="glyphicon glyphicon-edit"></span> Edit'.html_safe, edit_scenario_path(@scenario), class: "btn btn-default" %> |
|
18 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back', scenarios_path, class: "btn btn-default" %> |
|
19 |
+ <%= link_to icon_tag('glyphicon-random') + ' View Diagram', scenario_diagram_path(@scenario), class: "btn btn-default" %> |
|
20 |
+ <%= link_to icon_tag('glyphicon-edit') + ' Edit', edit_scenario_path(@scenario), class: "btn btn-default" %> |
|
21 | 21 |
<% if @scenario.source_url.present? %> |
22 |
- <%= link_to '<span class="glyphicon glyphicon-plus"></span> Update'.html_safe, new_scenario_imports_path(:url => @scenario.source_url), class: "btn btn-default" %> |
|
22 |
+ <%= link_to icon_tag('glyphicon-plus') + ' Update', new_scenario_imports_path(url: @scenario.source_url), class: "btn btn-default" %> |
|
23 | 23 |
<% end %> |
24 |
- <%= link_to '<span class="glyphicon glyphicon-share-alt"></span> Share'.html_safe, share_scenario_path(@scenario), class: "btn btn-default" %> |
|
25 |
- <%= link_to '<span class="glyphicon glyphicon-trash"></span> Delete'.html_safe, scenario_path(@scenario), method: :delete, data: { confirm: "This will remove the '#{@scenario.name}' Scenerio from all Agents and delete it. Are you sure?" }, class: "btn btn-default" %> |
|
24 |
+ <%= link_to icon_tag('glyphicon-share-alt') + ' Share', share_scenario_path(@scenario), class: "btn btn-default" %> |
|
25 |
+ <%= link_to icon_tag('glyphicon-trash') + ' Delete', scenario_path(@scenario), method: :delete, data: { confirm: "This will remove the '#{@scenario.name}' Scenerio from all Agents and delete it. Are you sure?" }, class: "btn btn-default" %> |
|
26 | 26 |
</div> |
27 | 27 |
</div> |
28 | 28 |
</div> |
@@ -12,7 +12,7 @@ |
||
12 | 12 |
for guidance. |
13 | 13 |
</p> |
14 | 14 |
<%- Devise.omniauth_providers.each { |provider| -%> |
15 |
- <p><%= link_to user_omniauth_authorize_path(provider), class: "btn btn-default btn-auth btn-auth-#{provider}" do %><%= icon_for_service(provider) %><span>Authenticate with <%= t("devise.omniauth_providers.#{provider}") %></span><% end %></p> |
|
15 |
+ <p><%= omniauth_button(provider) %></p> |
|
16 | 16 |
<%- } -%> |
17 | 17 |
<hr> |
18 | 18 |
|
@@ -27,9 +27,9 @@ |
||
27 | 27 |
|
28 | 28 |
<% @services.each do |service| %> |
29 | 29 |
<tr> |
30 |
- <td><%= service.provider %></td> |
|
30 |
+ <td><%= omniauth_provider_name(service.provider) %></td> |
|
31 | 31 |
<td><%= service.name %></td> |
32 |
- <td><%= service.global ? 'Yes' : 'No' %></td> |
|
32 |
+ <td><%= yes_no(service.global) %></td> |
|
33 | 33 |
<td> |
34 | 34 |
<div class="btn-group btn-group-xs"> |
35 | 35 |
<% if service.global %> |
@@ -13,9 +13,9 @@ |
||
13 | 13 |
|
14 | 14 |
<div class="row"> |
15 | 15 |
<div class="col-md-12"> |
16 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, user_credentials_path, class: "btn btn-default" %> |
|
16 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, user_credentials_path, class: "btn btn-default" %> |
|
17 | 17 |
</div> |
18 | 18 |
</div> |
19 | 19 |
</div> |
20 | 20 |
</div> |
21 |
-</div> |
|
21 |
+</div> |
@@ -38,7 +38,7 @@ |
||
38 | 38 |
|
39 | 39 |
<div class="btn-group"> |
40 | 40 |
<%= link_to new_user_credential_path, class: "btn btn-default" do %><span class="glyphicon glyphicon-plus"></span> New Credential<% end %> |
41 |
- <%= link_to user_credentials_path(format: :json), class: "btn btn-default" do %><span class="glyphicon glyphicon-download-alt"></span> Download Credentials<% end %> |
|
41 |
+ <%= link_to user_credentials_path(format: :json), class: "btn btn-default" do %><span class="glyphicon glyphicon-cloud-download"></span> Download Credentials<% end %> |
|
42 | 42 |
</div> |
43 | 43 |
</div> |
44 | 44 |
</div> |
@@ -13,9 +13,9 @@ |
||
13 | 13 |
|
14 | 14 |
<div class="row"> |
15 | 15 |
<div class="col-md-12"> |
16 |
- <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, user_credentials_path, class: "btn btn-default" %> |
|
16 |
+ <%= link_to icon_tag('glyphicon-chevron-left') + ' Back'.html_safe, user_credentials_path, class: "btn btn-default" %> |
|
17 | 17 |
</div> |
18 | 18 |
</div> |
19 | 19 |
</div> |
20 | 20 |
</div> |
21 |
-</div> |
|
21 |
+</div> |
@@ -11,5 +11,5 @@ unless defined?(Rails) |
||
11 | 11 |
exit 1 |
12 | 12 |
end |
13 | 13 |
|
14 |
-scheduler = HuginnScheduler.new |
|
15 |
-scheduler.run! |
|
14 |
+scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY']) |
|
15 |
+scheduler.run! |
@@ -33,7 +33,7 @@ end |
||
33 | 33 |
|
34 | 34 |
threads << Thread.new do |
35 | 35 |
safely do |
36 |
- @scheduler = HuginnScheduler.new |
|
36 |
+ @scheduler = HuginnScheduler.new(frequency: ENV['SCHEDULER_FREQUENCY']) |
|
37 | 37 |
@scheduler.run! |
38 | 38 |
puts "Scheduler stopped ..." |
39 | 39 |
end |
@@ -13,7 +13,7 @@ module Huginn |
||
13 | 13 |
# -- all .rb files in that directory are automatically loaded. |
14 | 14 |
|
15 | 15 |
# Custom directories with classes and modules you want to be autoloadable. |
16 |
- config.autoload_paths += %W(#{config.root}/lib) |
|
16 |
+ config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/presenters) |
|
17 | 17 |
|
18 | 18 |
# Activate observers that should always be running. |
19 | 19 |
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer |
@@ -3,4 +3,4 @@ require 'rubygems' |
||
3 | 3 |
# Set up gems listed in the Gemfile. |
4 | 4 |
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) |
5 | 5 |
|
6 |
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) |
|
6 |
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) |
@@ -7,3 +7,9 @@ Delayed::Worker.delay_jobs = !Rails.env.test? |
||
7 | 7 |
|
8 | 8 |
# Delayed::Worker.logger = Logger.new(Rails.root.join('log', 'delayed_job.log')) |
9 | 9 |
# Delayed::Worker.logger.level = Logger::DEBUG |
10 |
+ |
|
11 |
+class Delayed::Job |
|
12 |
+ scope :pending, ->{ where("locked_at IS NULL AND attempts = 0") } |
|
13 |
+ scope :awaiting_retry, ->{ where("failed_at IS NULL AND attempts > 0") } |
|
14 |
+ scope :failed, -> { where("failed_at IS NOT NULL") } |
|
15 |
+end |
@@ -5,10 +5,10 @@ Devise.setup do |config| |
||
5 | 5 |
# Configure the e-mail address which will be shown in Devise::Mailer, |
6 | 6 |
# note that it will be overwritten if you use your own mailer class |
7 | 7 |
# with default "from" parameter. |
8 |
- config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com" |
|
8 |
+ config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' |
|
9 | 9 |
|
10 | 10 |
# Configure the class responsible to send e-mails. |
11 |
- # config.mailer = "Devise::Mailer" |
|
11 |
+ # config.mailer = 'Devise::Mailer' |
|
12 | 12 |
|
13 | 13 |
# ==> ORM configuration |
14 | 14 |
# Load and configure the ORM. Supports :active_record (default) and |
@@ -49,17 +49,18 @@ Devise.setup do |config| |
||
49 | 49 |
# enable it only for database (email + password) authentication. |
50 | 50 |
# config.params_authenticatable = true |
51 | 51 |
|
52 |
- # Tell if authentication through HTTP Basic Auth is enabled. False by default. |
|
52 |
+ # Tell if authentication through HTTP Auth is enabled. False by default. |
|
53 | 53 |
# It can be set to an array that will enable http authentication only for the |
54 |
- # given strategies, for example, `config.http_authenticatable = [:token]` will |
|
55 |
- # enable it only for token authentication. |
|
54 |
+ # given strategies, for example, `config.http_authenticatable = [:database]` will |
|
55 |
+ # enable it only for database authentication. The supported strategies are: |
|
56 |
+ # :database = Support basic authentication with authentication key + password |
|
56 | 57 |
# config.http_authenticatable = false |
57 | 58 |
|
58 |
- # If http headers should be returned for AJAX requests. True by default. |
|
59 |
+ # If 401 status code should be returned for AJAX requests. True by default. |
|
59 | 60 |
# config.http_authenticatable_on_xhr = true |
60 | 61 |
|
61 |
- # The realm used in Http Basic Authentication. "Application" by default. |
|
62 |
- # config.http_authentication_realm = "Application" |
|
62 |
+ # The realm used in Http Basic Authentication. 'Application' by default. |
|
63 |
+ # config.http_authentication_realm = 'Application' |
|
63 | 64 |
|
64 | 65 |
# It will change confirmation, password recovery and other workflows |
65 | 66 |
# to behave the same regardless if the e-mail provided was right or wrong. |
@@ -67,10 +68,10 @@ Devise.setup do |config| |
||
67 | 68 |
# config.paranoid = true |
68 | 69 |
|
69 | 70 |
# By default Devise will store the user in session. You can skip storage for |
70 |
- # :http_auth and :token_auth by adding those symbols to the array below. |
|
71 |
+ # particular strategies by setting this option. |
|
71 | 72 |
# Notice that if you are skipping storage for all authentication paths, you |
72 | 73 |
# may want to disable generating routes to Devise's sessions controller by |
73 |
- # passing :skip => :sessions to `devise_for` in your config/routes.rb |
|
74 |
+ # passing skip: :sessions to `devise_for` in your config/routes.rb |
|
74 | 75 |
config.skip_session_storage = [:http_auth] |
75 | 76 |
|
76 | 77 |
# By default, Devise cleans up the CSRF token on authentication to |
@@ -85,7 +86,9 @@ Devise.setup do |config| |
||
85 | 86 |
# |
86 | 87 |
# Limiting the stretches to just one in testing will increase the performance of |
87 | 88 |
# your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use |
88 |
- # a value less than 10 in other environments. |
|
89 |
+ # a value less than 10 in other environments. Note that, for bcrypt (the default |
|
90 |
+ # encryptor), the cost increases exponentially with the number of stretches (e.g. |
|
91 |
+ # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). |
|
89 | 92 |
config.stretches = Rails.env.test? ? 1 : 10 |
90 | 93 |
|
91 | 94 |
# Setup a pepper to generate the encrypted password. |
@@ -93,16 +96,24 @@ Devise.setup do |config| |
||
93 | 96 |
|
94 | 97 |
# ==> Configuration for :confirmable |
95 | 98 |
# A period that the user is allowed to access the website even without |
96 |
- # confirming his account. For instance, if set to 2.days, the user will be |
|
97 |
- # able to access the website for two days without confirming his account, |
|
99 |
+ # confirming their account. For instance, if set to 2.days, the user will be |
|
100 |
+ # able to access the website for two days without confirming their account, |
|
98 | 101 |
# access will be blocked just in the third day. Default is 0.days, meaning |
99 |
- # the user cannot access the website without confirming his account. |
|
102 |
+ # the user cannot access the website without confirming their account. |
|
100 | 103 |
# config.allow_unconfirmed_access_for = 2.days |
101 | 104 |
|
105 |
+ # A period that the user is allowed to confirm their account before their |
|
106 |
+ # token becomes invalid. For example, if set to 3.days, the user can confirm |
|
107 |
+ # their account within 3 days after the mail was sent, but on the fourth day |
|
108 |
+ # their account can't be confirmed with the token any more. |
|
109 |
+ # Default is nil, meaning there is no restriction on how long a user can take |
|
110 |
+ # before confirming their account. |
|
111 |
+ # config.confirm_within = 3.days |
|
112 |
+ |
|
102 | 113 |
# If true, requires any email changes to be confirmed (exactly the same way as |
103 | 114 |
# initial account confirmation) to be applied. Requires additional unconfirmed_email |
104 |
- # db field (see migrations). Until confirmed new email is stored in |
|
105 |
- # unconfirmed email column, and copied to email column on successful confirmation. |
|
115 |
+ # db field (see migrations). Until confirmed, new email is stored in |
|
116 |
+ # unconfirmed_email column, and copied to email column on successful confirmation. |
|
106 | 117 |
config.reconfirmable = true |
107 | 118 |
|
108 | 119 |
# Defines which key will be used when confirming an account |
@@ -112,23 +123,26 @@ Devise.setup do |config| |
||
112 | 123 |
# The time the user will be remembered without asking for credentials again. |
113 | 124 |
config.remember_for = 4.weeks |
114 | 125 |
|
126 |
+ # Invalidates all the remember me tokens when the user signs out. |
|
127 |
+ config.expire_all_remember_me_on_sign_out = true |
|
128 |
+ |
|
115 | 129 |
# If true, extends the user's remember period when remembered via cookie. |
116 | 130 |
# config.extend_remember_period = false |
117 | 131 |
|
118 | 132 |
# Options to be passed to the created cookie. For instance, you can set |
119 |
- # :secure => true in order to force SSL only cookies. |
|
133 |
+ # secure: true in order to force SSL only cookies. |
|
120 | 134 |
if Rails.env.production? |
121 |
- config.rememberable_options = { :secure => true } |
|
135 |
+ config.rememberable_options = { secure: true } |
|
122 | 136 |
else |
123 | 137 |
config.rememberable_options = { } |
124 | 138 |
end |
125 | 139 |
|
126 | 140 |
# ==> Configuration for :validatable |
127 |
- # Range for password length. Default is 6..128. |
|
128 |
- # config.password_length = 6..128 |
|
141 |
+ # Range for password length. |
|
142 |
+ config.password_length = 8..128 |
|
129 | 143 |
|
130 | 144 |
# Email regex used to validate email formats. It simply asserts that |
131 |
- # an one (and only one) @ exists in the given string. This is mainly |
|
145 |
+ # one (and only one) @ exists in the given string. This is mainly |
|
132 | 146 |
# to give user feedback and not to assert the e-mail validity. |
133 | 147 |
# config.email_regexp = /\A[^@]+@[^@]+\z/ |
134 | 148 |
|
@@ -136,7 +150,7 @@ Devise.setup do |config| |
||
136 | 150 |
# The time you want to timeout the user session without activity. After this |
137 | 151 |
# time the user will be asked for credentials again. Default is 30 minutes. |
138 | 152 |
# config.timeout_in = 30.minutes |
139 |
- |
|
153 |
+ |
|
140 | 154 |
# If true, expires auth token on session timeout. |
141 | 155 |
# config.expire_auth_token_on_timeout = false |
142 | 156 |
|
@@ -163,6 +177,9 @@ Devise.setup do |config| |
||
163 | 177 |
# Time interval to unlock the account if :time is enabled as unlock_strategy. |
164 | 178 |
config.unlock_in = 1.hour |
165 | 179 |
|
180 |
+ # Warn on the last attempt before the account is locked. |
|
181 |
+ # config.last_attempt_warning = true |
|
182 |
+ |
|
166 | 183 |
# ==> Configuration for :recoverable |
167 | 184 |
# |
168 | 185 |
# Defines which key will be used when recovering the password for an account |
@@ -178,7 +195,9 @@ Devise.setup do |config| |
||
178 | 195 |
# :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, |
179 | 196 |
# :authlogic_sha512 (then you should set stretches above to 20 for default behavior) |
180 | 197 |
# and :restful_authentication_sha1 (then you should set stretches to 10, and copy |
181 |
- # REST_AUTH_SITE_KEY to pepper) |
|
198 |
+ # REST_AUTH_SITE_KEY to pepper). |
|
199 |
+ # |
|
200 |
+ # Require the `devise-encryptable` gem when using anything other than bcrypt |
|
182 | 201 |
# config.encryptor = :sha512 |
183 | 202 |
|
184 | 203 |
# ==> Scopes configuration |
@@ -204,7 +223,7 @@ Devise.setup do |config| |
||
204 | 223 |
# should add them to the navigational formats lists. |
205 | 224 |
# |
206 | 225 |
# The "*/*" below is required to match Internet Explorer requests. |
207 |
- # config.navigational_formats = ["*/*", :html] |
|
226 |
+ # config.navigational_formats = ['*/*', :html] |
|
208 | 227 |
|
209 | 228 |
# The default HTTP method used to sign out a resource. Default is :delete. |
210 | 229 |
config.sign_out_via = :get |
@@ -212,7 +231,8 @@ Devise.setup do |config| |
||
212 | 231 |
# ==> OmniAuth |
213 | 232 |
# Add a new OmniAuth provider. Check the wiki for more information on setting |
214 | 233 |
# up on your models and hooks. |
215 |
- # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo' |
|
234 |
+ # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' |
|
235 |
+ |
|
216 | 236 |
if defined?(OmniAuth::Strategies::Twitter) && |
217 | 237 |
(key = ENV["TWITTER_OAUTH_KEY"]).present? && |
218 | 238 |
(secret = ENV["TWITTER_OAUTH_SECRET"]).present? |
@@ -237,13 +257,19 @@ Devise.setup do |config| |
||
237 | 257 |
config.omniauth :github, key, secret |
238 | 258 |
end |
239 | 259 |
|
260 |
+ if defined?(OmniAuth::Strategies::Dropbox) && |
|
261 |
+ (key = ENV["DROPBOX_OAUTH_KEY"]).present? && |
|
262 |
+ (secret = ENV["DROPBOX_OAUTH_SECRET"]).present? |
|
263 |
+ config.omniauth :dropbox, key, secret |
|
264 |
+ end |
|
265 |
+ |
|
240 | 266 |
# ==> Warden configuration |
241 | 267 |
# If you want to use other strategies, that are not supported by Devise, or |
242 | 268 |
# change the failure app, you can configure them inside the config.warden block. |
243 | 269 |
# |
244 | 270 |
# config.warden do |manager| |
245 | 271 |
# manager.intercept_401 = false |
246 |
- # manager.default_strategies(:scope => :user).unshift :some_external_strategy |
|
272 |
+ # manager.default_strategies(scope: :user).unshift :some_external_strategy |
|
247 | 273 |
# end |
248 | 274 |
|
249 | 275 |
# ==> Mountable engine configurations |
@@ -251,13 +277,13 @@ Devise.setup do |config| |
||
251 | 277 |
# is mountable, there are some extra configurations to be taken into account. |
252 | 278 |
# The following options are available, assuming the engine is mounted as: |
253 | 279 |
# |
254 |
- # mount MyEngine, at: "/my_engine" |
|
280 |
+ # mount MyEngine, at: '/my_engine' |
|
255 | 281 |
# |
256 | 282 |
# The router that invoked `devise_for`, in the example above, would be: |
257 | 283 |
# config.router_name = :my_engine |
258 | 284 |
# |
259 | 285 |
# When using omniauth, Devise cannot automatically set Omniauth path, |
260 | 286 |
# so you need to do it manually. For the users scope, it would be: |
261 |
- # config.omniauth_path_prefix = "/my_engine/users/auth" |
|
287 |
+ # config.omniauth_path_prefix = '/my_engine/users/auth' |
|
262 | 288 |
config.omniauth_path_prefix = "/auth" |
263 | 289 |
end |
@@ -1,63 +1,66 @@ |
||
1 | 1 |
# Additional translations at https://github.com/plataformatec/devise/wiki/I18n |
2 | 2 |
|
3 | 3 |
en: |
4 |
+ devise: |
|
5 |
+ confirmations: |
|
6 |
+ confirmed: "Your email address has been successfully confirmed." |
|
7 |
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." |
|
8 |
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." |
|
9 |
+ failure: |
|
10 |
+ already_authenticated: "You are already signed in." |
|
11 |
+ inactive: "Your account is not activated yet." |
|
12 |
+ invalid: "Invalid %{authentication_keys} or password." |
|
13 |
+ locked: "Your account is locked." |
|
14 |
+ last_attempt: "You have one more attempt before your account is locked." |
|
15 |
+ not_found_in_database: "Invalid %{authentication_keys} or password." |
|
16 |
+ timeout: "Your session expired. Please sign in again to continue." |
|
17 |
+ unauthenticated: "You need to sign in or sign up before continuing." |
|
18 |
+ unconfirmed: "You have to confirm your email address before continuing." |
|
19 |
+ mailer: |
|
20 |
+ confirmation_instructions: |
|
21 |
+ subject: "Confirmation instructions" |
|
22 |
+ reset_password_instructions: |
|
23 |
+ subject: "Reset password instructions" |
|
24 |
+ unlock_instructions: |
|
25 |
+ subject: "Unlock instructions" |
|
26 |
+ omniauth_callbacks: |
|
27 |
+ failure: "Could not authenticate you from %{kind} because \"%{reason}\"." |
|
28 |
+ success: "Successfully authenticated from %{kind} account." |
|
29 |
+ omniauth_providers: |
|
30 |
+ twitter: "Twitter" |
|
31 |
+ tumblr: "Tumblr" |
|
32 |
+ github: "GitHub" |
|
33 |
+ 37signals: "37Signals (Basecamp)" |
|
34 |
+ dropbox: "Dropbox" |
|
35 |
+ passwords: |
|
36 |
+ no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." |
|
37 |
+ send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." |
|
38 |
+ send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." |
|
39 |
+ updated: "Your password has been changed successfully. You are now signed in." |
|
40 |
+ updated_not_active: "Your password has been changed successfully." |
|
41 |
+ registrations: |
|
42 |
+ destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." |
|
43 |
+ signed_up: "Welcome! You have signed up successfully." |
|
44 |
+ signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." |
|
45 |
+ signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." |
|
46 |
+ signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." |
|
47 |
+ update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." |
|
48 |
+ updated: "Your account has been updated successfully." |
|
49 |
+ sessions: |
|
50 |
+ signed_in: "Signed in successfully." |
|
51 |
+ signed_out: "Signed out successfully." |
|
52 |
+ already_signed_out: "Signed out successfully." |
|
53 |
+ unlocks: |
|
54 |
+ send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." |
|
55 |
+ send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." |
|
56 |
+ unlocked: "Your account has been unlocked successfully. Please sign in to continue." |
|
4 | 57 |
errors: |
5 | 58 |
messages: |
59 |
+ already_confirmed: "was already confirmed, please try signing in" |
|
60 |
+ confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" |
|
6 | 61 |
expired: "has expired, please request a new one" |
7 | 62 |
not_found: "not found" |
8 |
- already_confirmed: "was already confirmed, please try signing in" |
|
9 | 63 |
not_locked: "was not locked" |
10 | 64 |
not_saved: |
11 | 65 |
one: "1 error prohibited this %{resource} from being saved:" |
12 | 66 |
other: "%{count} errors prohibited this %{resource} from being saved:" |
13 |
- |
|
14 |
- devise: |
|
15 |
- failure: |
|
16 |
- already_authenticated: 'You are already signed in.' |
|
17 |
- unauthenticated: 'You need to sign in or sign up before continuing.' |
|
18 |
- unconfirmed: 'You have to confirm your account before continuing.' |
|
19 |
- locked: 'Your account is locked.' |
|
20 |
- invalid: 'Invalid login or password.' |
|
21 |
- invalid_token: 'Invalid authentication token.' |
|
22 |
- timeout: 'Your session expired, please sign in again to continue.' |
|
23 |
- inactive: 'Your account was not activated yet.' |
|
24 |
- sessions: |
|
25 |
- signed_in: 'Signed in successfully.' |
|
26 |
- signed_out: 'Signed out successfully.' |
|
27 |
- passwords: |
|
28 |
- send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.' |
|
29 |
- updated: 'Your password was changed successfully. You are now signed in.' |
|
30 |
- updated_not_active: 'Your password was changed successfully.' |
|
31 |
- send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." |
|
32 |
- no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." |
|
33 |
- confirmations: |
|
34 |
- send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.' |
|
35 |
- send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.' |
|
36 |
- confirmed: 'Your account was successfully confirmed. You are now signed in.' |
|
37 |
- registrations: |
|
38 |
- signed_up: 'Welcome! You have signed up successfully.' |
|
39 |
- signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.' |
|
40 |
- signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.' |
|
41 |
- signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.' |
|
42 |
- updated: 'You updated your account successfully.' |
|
43 |
- update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address." |
|
44 |
- destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' |
|
45 |
- unlocks: |
|
46 |
- send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' |
|
47 |
- unlocked: 'Your account has been unlocked successfully. Please sign in to continue.' |
|
48 |
- send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.' |
|
49 |
- omniauth_callbacks: |
|
50 |
- success: 'Successfully authenticated from %{kind} account.' |
|
51 |
- failure: 'Could not authenticate you from %{kind} because "%{reason}".' |
|
52 |
- omniauth_providers: |
|
53 |
- twitter: 'Twitter' |
|
54 |
- tumblr: 'Tumblr' |
|
55 |
- github: 'GitHub' |
|
56 |
- 37signals: '37Signals (Basecamp)' |
|
57 |
- mailer: |
|
58 |
- confirmation_instructions: |
|
59 |
- subject: 'Confirmation instructions' |
|
60 |
- reset_password_instructions: |
|
61 |
- subject: 'Reset password instructions' |
|
62 |
- unlock_instructions: |
|
63 |
- subject: 'Unlock Instructions' |
@@ -11,6 +11,8 @@ Huginn::Application.routes.draw do |
||
11 | 11 |
post :propagate |
12 | 12 |
get :type_details |
13 | 13 |
get :event_descriptions |
14 |
+ post :validate |
|
15 |
+ post :complete |
|
14 | 16 |
end |
15 | 17 |
|
16 | 18 |
resources :logs, :only => [:index] do |
@@ -66,9 +68,9 @@ Huginn::Application.routes.draw do |
||
66 | 68 |
post "/users/:user_id/webhooks/:agent_id/:secret" => "web_requests#handle_request" # legacy |
67 | 69 |
post "/users/:user_id/update_location/:secret" => "web_requests#update_location" # legacy |
68 | 70 |
|
69 |
- match '/auth/:provider/callback', to: 'services#callback', |
|
70 |
- via: [:get, :post] #, constraints: { provider: Regexp.union(Devise.omniauth_providers.map(&:to_s)) } |
|
71 |
- devise_for :users, :sign_out_via => [ :post, :delete ] |
|
71 |
+ devise_for :users, |
|
72 |
+ controllers: { omniauth_callbacks: 'omniauth_callbacks' }, |
|
73 |
+ sign_out_via: [:post, :delete] |
|
72 | 74 |
|
73 | 75 |
get "/about" => "home#about" |
74 | 76 |
root :to => "home#index" |
@@ -20,7 +20,7 @@ before_fork do |server, worker| |
||
20 | 20 |
defined?(ActiveRecord::Base) and |
21 | 21 |
ActiveRecord::Base.connection.disconnect! |
22 | 22 |
old_pid = "#{server.config[:pid]}.oldbin" |
23 |
- if File.exists?(old_pid) && server.pid != old_pid |
|
23 |
+ if File.exist?(old_pid) && server.pid != old_pid |
|
24 | 24 |
begin |
25 | 25 |
Process.kill("QUIT", File.read(old_pid).to_i) |
26 | 26 |
rescue Errno::ENOENT, Errno::ESRCH |
@@ -19,7 +19,7 @@ pid '/home/you/app/shared/pids/unicorn.pid' |
||
19 | 19 |
before_fork do |server, worker| |
20 | 20 |
ActiveRecord::Base.connection.disconnect! |
21 | 21 |
old_pid = "#{server.config[:pid]}.oldbin" |
22 |
- if File.exists?(old_pid) && server.pid != old_pid |
|
22 |
+ if File.exist?(old_pid) && server.pid != old_pid |
|
23 | 23 |
begin |
24 | 24 |
Process.kill("QUIT", File.read(old_pid).to_i) |
25 | 25 |
rescue Errno::ENOENT, Errno::ESRCH |
@@ -90,7 +90,7 @@ These are: |
||
90 | 90 |
HUGINN_RAILS_ENV |
91 | 91 |
HUGINN_FORCE_SSL |
92 | 92 |
HUGINN_INVITATION_CODE |
93 |
- HUGINN_SMTP_DOMAIM |
|
93 |
+ HUGINN_SMTP_DOMAIN |
|
94 | 94 |
HUGINN_SMTP_USER_NAME |
95 | 95 |
HUGINN_SMTP_PASSWORD |
96 | 96 |
HUGINN_SMTP_SERVER |
@@ -96,8 +96,8 @@ class HuginnScheduler |
||
96 | 96 |
FAILED_JOBS_TO_KEEP = 100 |
97 | 97 |
attr_accessor :mutex |
98 | 98 |
|
99 |
- def initialize |
|
100 |
- @rufus_scheduler = Rufus::Scheduler.new |
|
99 |
+ def initialize(options = {}) |
|
100 |
+ @rufus_scheduler = Rufus::Scheduler.new(options) |
|
101 | 101 |
self.mutex = Mutex.new |
102 | 102 |
end |
103 | 103 |
|
@@ -0,0 +1,56 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe FormConfigurable do |
|
4 |
+ class Agent1 |
|
5 |
+ include FormConfigurable |
|
6 |
+ |
|
7 |
+ def validate_test |
|
8 |
+ true |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ def complete_test |
|
12 |
+ [{name: 'test', value: 1234}] |
|
13 |
+ end |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ class Agent2 < Agent |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ before(:all) do |
|
20 |
+ @agent1 = Agent1.new |
|
21 |
+ @agent2 = Agent2.new |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ it "#is_form_configurable" do |
|
25 |
+ expect(@agent1.is_form_configurable?).to be true |
|
26 |
+ expect(@agent2.is_form_configurable?).to be false |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ describe "#validete_option" do |
|
30 |
+ it "should call the validation method if it is defined" do |
|
31 |
+ expect(@agent1.validate_option('test')).to be true |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ it "should return false of the method is undefined" do |
|
35 |
+ expect(@agent1.validate_option('undefined')).to be false |
|
36 |
+ end |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ it "#complete_option" do |
|
40 |
+ expect(@agent1.complete_option('test')).to eq [{name: 'test', value: 1234}] |
|
41 |
+ end |
|
42 |
+ |
|
43 |
+ describe "#form_configurable" do |
|
44 |
+ it "should raise an ArgumentError for invalid options" do |
|
45 |
+ expect { Agent1.form_configurable(:test, invalid: true) }.to raise_error(ArgumentError) |
|
46 |
+ end |
|
47 |
+ |
|
48 |
+ it "should raise an ArgumentError when not providing an array with type: array" do |
|
49 |
+ expect { Agent1.form_configurable(:test, type: :array, values: 1) }.to raise_error(ArgumentError) |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ it "should not require any options for the default values" do |
|
53 |
+ expect { Agent1.form_configurable(:test) }.to change(Agent1, :form_configurable_attributes).by(['test']) |
|
54 |
+ end |
|
55 |
+ end |
|
56 |
+end |
@@ -307,4 +307,44 @@ describe AgentsController do |
||
307 | 307 |
expect(response).to redirect_to scenario_path(scenarios(:bob_weather)) |
308 | 308 |
end |
309 | 309 |
end |
310 |
+ |
|
311 |
+ describe "#form_configurable actions" do |
|
312 |
+ before(:each) do |
|
313 |
+ @params = {attribute: 'auth_token', agent: valid_attributes(:type => "Agents::HipchatAgent", options: {auth_token: '12345'})} |
|
314 |
+ sign_in users(:bob) |
|
315 |
+ end |
|
316 |
+ describe "POST validate" do |
|
317 |
+ |
|
318 |
+ it "returns with status 200 when called with a valid option" do |
|
319 |
+ any_instance_of(Agents::HipchatAgent) do |klass| |
|
320 |
+ stub(klass).validate_option { true } |
|
321 |
+ end |
|
322 |
+ |
|
323 |
+ post :validate, @params |
|
324 |
+ expect(response.status).to eq 200 |
|
325 |
+ end |
|
326 |
+ |
|
327 |
+ it "returns with status 403 when called with an invalid option" do |
|
328 |
+ any_instance_of(Agents::HipchatAgent) do |klass| |
|
329 |
+ stub(klass).validate_option { false } |
|
330 |
+ end |
|
331 |
+ |
|
332 |
+ post :validate, @params |
|
333 |
+ expect(response.status).to eq 403 |
|
334 |
+ end |
|
335 |
+ end |
|
336 |
+ |
|
337 |
+ describe "POST complete" do |
|
338 |
+ it "callsAgent#complete_option and renders json" do |
|
339 |
+ any_instance_of(Agents::HipchatAgent) do |klass| |
|
340 |
+ stub(klass).complete_option { [{name: 'test', value: 1}] } |
|
341 |
+ end |
|
342 |
+ |
|
343 |
+ post :complete, @params |
|
344 |
+ expect(response.status).to eq 200 |
|
345 |
+ expect(response.header['Content-Type']).to include('application/json') |
|
346 |
+ |
|
347 |
+ end |
|
348 |
+ end |
|
349 |
+ end |
|
310 | 350 |
end |
@@ -0,0 +1,26 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe OmniauthCallbacksController do |
|
4 |
+ before do |
|
5 |
+ sign_in users(:bob) |
|
6 |
+ OmniAuth.config.test_mode = true |
|
7 |
+ request.env["devise.mapping"] = Devise.mappings[:user] |
|
8 |
+ request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json'))) |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ describe "accepting a callback url" do |
|
12 |
+ it "should update the user's credentials" do |
|
13 |
+ expect { |
|
14 |
+ get :twitter |
|
15 |
+ }.to change { users(:bob).services.count }.by(1) |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ # it "should work with an unknown provider (for now)" do |
|
19 |
+ # request.env["omniauth.auth"]['provider'] = 'unknown' |
|
20 |
+ # expect { |
|
21 |
+ # get :unknown |
|
22 |
+ # }.to change { users(:bob).services.count }.by(1) |
|
23 |
+ # expect(users(:bob).services.first.provider).to eq('unknown') |
|
24 |
+ # end |
|
25 |
+ end |
|
26 |
+end |
@@ -28,7 +28,7 @@ describe ScenariosController do |
||
28 | 28 |
|
29 | 29 |
it "loads Agents for the requested Scenario" do |
30 | 30 |
get :show, :id => scenarios(:bob_weather).to_param |
31 |
- expect(assigns(:agents).pluck(:id)).to eq(scenarios(:bob_weather).agents.pluck(:id)) |
|
31 |
+ expect(assigns(:agents).pluck(:id).sort).to eq(scenarios(:bob_weather).agents.pluck(:id).sort) |
|
32 | 32 |
end |
33 | 33 |
end |
34 | 34 |
|
@@ -3,8 +3,6 @@ require 'spec_helper' |
||
3 | 3 |
describe ServicesController do |
4 | 4 |
before do |
5 | 5 |
sign_in users(:bob) |
6 |
- OmniAuth.config.test_mode = true |
|
7 |
- request.env["omniauth.auth"] = JSON.parse(File.read(Rails.root.join('spec/data_fixtures/services/twitter.json'))) |
|
8 | 6 |
end |
9 | 7 |
|
10 | 8 |
describe "GET index" do |
@@ -39,20 +37,4 @@ describe ServicesController do |
||
39 | 37 |
}.to raise_error(ActiveRecord::RecordNotFound) |
40 | 38 |
end |
41 | 39 |
end |
42 |
- |
|
43 |
- describe "accepting a callback url" do |
|
44 |
- it "should update the user's credentials" do |
|
45 |
- expect { |
|
46 |
- get :callback, provider: 'twitter' |
|
47 |
- }.to change { users(:bob).services.count }.by(1) |
|
48 |
- end |
|
49 |
- |
|
50 |
- it "should work with an unknown provider (for now)" do |
|
51 |
- request.env["omniauth.auth"]['provider'] = 'unknown' |
|
52 |
- expect { |
|
53 |
- get :callback, provider: 'unknown' |
|
54 |
- }.to change { users(:bob).services.count }.by(1) |
|
55 |
- expect(users(:bob).services.first.provider).to eq('unknown') |
|
56 |
- end |
|
57 |
- end |
|
58 | 40 |
end |
@@ -5,4 +5,6 @@ TUMBLR_OAUTH_KEY=tumblroauthsecret |
||
5 | 5 |
TUMBLR_OAUTH_SECRET=tumblroauthsecret |
6 | 6 |
THIRTY_SEVEN_SIGNALS_OAUTH_KEY=TESTKEY |
7 | 7 |
THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET |
8 |
+DROPBOX_OAUTH_KEY=dropboxoauthkey |
|
9 |
+DROPBOX_OAUTH_SECRET=dropboxoauthsecret |
|
8 | 10 |
FAILED_JOBS_TO_KEEP=2 |
@@ -1,6 +1,32 @@ |
||
1 | 1 |
require 'spec_helper' |
2 | 2 |
|
3 | 3 |
describe ApplicationHelper do |
4 |
+ describe '#icon_tag' do |
|
5 |
+ it 'returns a Glyphicon icon element' do |
|
6 |
+ icon = icon_tag('glyphicon-help') |
|
7 |
+ expect(icon).to be_html_safe |
|
8 |
+ expect(Nokogiri(icon).at('span.glyphicon.glyphicon-help')).to be_a Nokogiri::XML::Element |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ it 'returns a Glyphicon icon element with an addidional class' do |
|
12 |
+ icon = icon_tag('glyphicon-help', class: 'text-info') |
|
13 |
+ expect(icon).to be_html_safe |
|
14 |
+ expect(Nokogiri(icon).at('span.glyphicon.glyphicon-help.text-info')).to be_a Nokogiri::XML::Element |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ it 'returns a FontAwesome icon element' do |
|
18 |
+ icon = icon_tag('fa-copy') |
|
19 |
+ expect(icon).to be_html_safe |
|
20 |
+ expect(Nokogiri(icon).at('i.fa.fa-copy')).to be_a Nokogiri::XML::Element |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ it 'returns a FontAwesome icon element' do |
|
24 |
+ icon = icon_tag('fa-copy', class: 'text-info') |
|
25 |
+ expect(icon).to be_html_safe |
|
26 |
+ expect(Nokogiri(icon).at('i.fa.fa-copy.text-info')).to be_a Nokogiri::XML::Element |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ |
|
4 | 30 |
describe '#nav_link' do |
5 | 31 |
it 'returns a nav link' do |
6 | 32 |
stub(self).current_page?('/things') { false } |
@@ -9,15 +35,6 @@ describe ApplicationHelper do |
||
9 | 35 |
expect(a.text.strip).to eq('Things') |
10 | 36 |
end |
11 | 37 |
|
12 |
- it 'returns a nav link with a glyphicon' do |
|
13 |
- stub(self).current_page?('/things') { false } |
|
14 |
- nav = nav_link('Things', '/things', glyphicon: 'help') |
|
15 |
- expect(nav).to be_html_safe |
|
16 |
- a = Nokogiri(nav).at('li:not(.active) > a[href="/things"]') |
|
17 |
- expect(a.at('span.glyphicon.glyphicon-help')).to be_a Nokogiri::XML::Element |
|
18 |
- expect(a.text.strip).to eq('Things') |
|
19 |
- end |
|
20 |
- |
|
21 | 38 |
it 'returns an active nav link' do |
22 | 39 |
stub(self).current_page?('/things') { true } |
23 | 40 |
nav = nav_link('Things', '/things') |
@@ -121,26 +138,58 @@ describe ApplicationHelper do |
||
121 | 138 |
end |
122 | 139 |
end |
123 | 140 |
|
124 |
- describe '#icon_for_service' do |
|
141 |
+ describe '#omniauth_provider_icon' do |
|
125 | 142 |
it 'returns a correct icon tag for Twitter' do |
126 |
- icon = icon_for_service(:twitter) |
|
143 |
+ icon = omniauth_provider_icon(:twitter) |
|
127 | 144 |
expect(icon).to be_html_safe |
128 | 145 |
elem = Nokogiri(icon).at('i.fa.fa-twitter') |
129 | 146 |
expect(elem).to be_a Nokogiri::XML::Element |
130 | 147 |
end |
131 | 148 |
|
132 | 149 |
it 'returns a correct icon tag for GitHub' do |
133 |
- icon = icon_for_service(:github) |
|
150 |
+ icon = omniauth_provider_icon(:github) |
|
134 | 151 |
expect(icon).to be_html_safe |
135 | 152 |
elem = Nokogiri(icon).at('i.fa.fa-github') |
136 | 153 |
expect(elem).to be_a Nokogiri::XML::Element |
137 | 154 |
end |
138 | 155 |
|
139 | 156 |
it 'returns a correct icon tag for other services' do |
140 |
- icon = icon_for_service(:'37signals') |
|
157 |
+ icon = omniauth_provider_icon(:'37signals') |
|
141 | 158 |
expect(icon).to be_html_safe |
142 | 159 |
elem = Nokogiri(icon).at('i.fa.fa-lock') |
143 | 160 |
expect(elem).to be_a Nokogiri::XML::Element |
144 | 161 |
end |
145 | 162 |
end |
163 |
+ |
|
164 |
+ describe '#highlighted?' do |
|
165 |
+ it 'understands hl=6-8' do |
|
166 |
+ stub(params).[](:hl) { '6-8' } |
|
167 |
+ expect((1..10).select { |i| highlighted?(i) }).to eq [6, 7, 8] |
|
168 |
+ end |
|
169 |
+ |
|
170 |
+ it 'understands hl=1,3-4,9' do |
|
171 |
+ stub(params).[](:hl) { '1,3-4,9' } |
|
172 |
+ expect((1..10).select { |i| highlighted?(i) }).to eq [1, 3, 4, 9] |
|
173 |
+ end |
|
174 |
+ |
|
175 |
+ it 'understands hl=8-' do |
|
176 |
+ stub(params).[](:hl) { '8-' } |
|
177 |
+ expect((1..10).select { |i| highlighted?(i) }).to eq [8, 9, 10] |
|
178 |
+ end |
|
179 |
+ |
|
180 |
+ it 'understands hl=-2' do |
|
181 |
+ stub(params).[](:hl) { '-2' } |
|
182 |
+ expect((1..10).select { |i| highlighted?(i) }).to eq [1, 2] |
|
183 |
+ end |
|
184 |
+ |
|
185 |
+ it 'understands hl=-' do |
|
186 |
+ stub(params).[](:hl) { '-' } |
|
187 |
+ expect((1..10).select { |i| highlighted?(i) }).to eq [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
|
188 |
+ end |
|
189 |
+ |
|
190 |
+ it 'is OK with no hl' do |
|
191 |
+ stub(params).[](:hl) { nil } |
|
192 |
+ expect((1..10).select { |i| highlighted?(i) }).to be_empty |
|
193 |
+ end |
|
194 |
+ end |
|
146 | 195 |
end |
@@ -86,11 +86,11 @@ describe Rufus::Scheduler do |
||
86 | 86 |
|
87 | 87 |
stub.any_instance_of(Agents::SchedulerAgent).second_precision_enabled { true } |
88 | 88 |
|
89 |
- @agent1 = Agents::SchedulerAgent.new(name: 'Scheduler 1', options: { schedule: '*/1 * * * * *' }).tap { |a| |
|
89 |
+ @agent1 = Agents::SchedulerAgent.new(name: 'Scheduler 1', options: { action: 'run', schedule: '*/1 * * * * *' }).tap { |a| |
|
90 | 90 |
a.user = users(:bob) |
91 | 91 |
a.save! |
92 | 92 |
} |
93 |
- @agent2 = Agents::SchedulerAgent.new(name: 'Scheduler 2', options: { schedule: '*/1 * * * * *' }).tap { |a| |
|
93 |
+ @agent2 = Agents::SchedulerAgent.new(name: 'Scheduler 2', options: { action: 'run', schedule: '*/1 * * * * *' }).tap { |a| |
|
94 | 94 |
a.user = users(:bob) |
95 | 95 |
a.save! |
96 | 96 |
} |
@@ -107,7 +107,7 @@ describe Rufus::Scheduler do |
||
107 | 107 |
it 'registers active SchedulerAgents' do |
108 | 108 |
@scheduler.schedule_scheduler_agents |
109 | 109 |
|
110 |
- expect(@scheduler.scheduler_agent_jobs.map(&:scheduler_agent)).to eq([@agent1, @agent2]) |
|
110 |
+ expect(@scheduler.scheduler_agent_jobs.map(&:scheduler_agent).sort_by(&:id)).to eq([@agent1, @agent2]) |
|
111 | 111 |
end |
112 | 112 |
|
113 | 113 |
it 'unregisters disabled SchedulerAgents' do |
@@ -44,9 +44,9 @@ describe AgentLog do |
||
44 | 44 |
|
45 | 45 |
it "truncates message to a reasonable length" do |
46 | 46 |
log = AgentLog.new(:agent => agents(:jane_website_agent), :level => 3) |
47 |
- log.message = "a" * 3000 |
|
47 |
+ log.message = "a" * 11_000 |
|
48 | 48 |
log.save! |
49 |
- expect(log.message.length).to eq(2048) |
|
49 |
+ expect(log.message.length).to eq(10_000) |
|
50 | 50 |
end |
51 | 51 |
|
52 | 52 |
describe "#log_for_agent" do |
@@ -561,12 +561,15 @@ describe Agent do |
||
561 | 561 |
|
562 | 562 |
describe "cleaning up now-expired events" do |
563 | 563 |
before do |
564 |
- @agent = Agents::SomethingSource.new(:name => "something") |
|
565 |
- @agent.keep_events_for = 5 |
|
566 |
- @agent.user = users(:bob) |
|
567 |
- @agent.save! |
|
568 |
- @event = @agent.create_event :payload => { "hello" => "world" } |
|
569 |
- expect(@event.expires_at.to_i).to be_within(2).of(5.days.from_now.to_i) |
|
564 |
+ @time = "2014-01-01 01:00:00 +00:00" |
|
565 |
+ time_travel_to @time do |
|
566 |
+ @agent = Agents::SomethingSource.new(:name => "something") |
|
567 |
+ @agent.keep_events_for = 5 |
|
568 |
+ @agent.user = users(:bob) |
|
569 |
+ @agent.save! |
|
570 |
+ @event = @agent.create_event :payload => { "hello" => "world" } |
|
571 |
+ expect(@event.expires_at.to_i).to be_within(2).of(5.days.from_now.to_i) |
|
572 |
+ end |
|
570 | 573 |
end |
571 | 574 |
|
572 | 575 |
describe "when keep_events_for has not changed" do |
@@ -584,12 +587,14 @@ describe Agent do |
||
584 | 587 |
|
585 | 588 |
describe "when keep_events_for is changed" do |
586 | 589 |
it "updates events' expires_at" do |
587 |
- expect { |
|
588 |
- @agent.options[:foo] = "bar1" |
|
589 |
- @agent.keep_events_for = 3 |
|
590 |
- @agent.save! |
|
591 |
- }.to change { @event.reload.expires_at } |
|
592 |
- expect(@event.expires_at.to_i).to be_within(2).of(3.days.from_now.to_i) |
|
590 |
+ time_travel_to @time do |
|
591 |
+ expect { |
|
592 |
+ @agent.options[:foo] = "bar1" |
|
593 |
+ @agent.keep_events_for = 3 |
|
594 |
+ @agent.save! |
|
595 |
+ }.to change { @event.reload.expires_at } |
|
596 |
+ expect(@event.expires_at.to_i).to be_within(2).of(3.days.from_now.to_i) |
|
597 |
+ end |
|
593 | 598 |
end |
594 | 599 |
|
595 | 600 |
it "updates events relative to their created_at" do |
@@ -5,8 +5,21 @@ describe Agents::BasecampAgent do |
||
5 | 5 |
it_behaves_like Oauthable |
6 | 6 |
|
7 | 7 |
before(:each) do |
8 |
- stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
9 |
- stub_request(:get, /02:00$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
8 |
+ stub_request(:get, /events.json$/).to_return( |
|
9 |
+ :body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), |
|
10 |
+ :status => 200, |
|
11 |
+ :headers => {"Content-Type" => "text/json"} |
|
12 |
+ ) |
|
13 |
+ stub_request(:get, /projects.json$/).to_return( |
|
14 |
+ :body => JSON.dump([{name: 'test', id: 1234},{name: 'test1', id: 1235}]), |
|
15 |
+ :status => 200, |
|
16 |
+ :headers => {"Content-Type" => "text/json"} |
|
17 |
+ ) |
|
18 |
+ stub_request(:get, /02:00$/).to_return( |
|
19 |
+ :body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), |
|
20 |
+ :status => 200, |
|
21 |
+ :headers => {"Content-Type" => "text/json"} |
|
22 |
+ ) |
|
10 | 23 |
@valid_params = { :project_id => 6789 } |
11 | 24 |
|
12 | 25 |
@checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params) |
@@ -32,10 +45,13 @@ describe Agents::BasecampAgent do |
||
32 | 45 |
expect(@checker.send(:request_options)).to eq({:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => 'Bearer "1234token"'}}) |
33 | 46 |
end |
34 | 47 |
|
35 |
- it "should generate the currect request url" do |
|
36 |
- expect(@checker.send(:request_url)).to eq("https://basecamp.com/12345/api/v1/projects/6789/events.json") |
|
48 |
+ it "should generate the correct events url" do |
|
49 |
+ expect(@checker.send(:events_url)).to eq("https://basecamp.com/12345/api/v1/projects/6789/events.json") |
|
37 | 50 |
end |
38 | 51 |
|
52 |
+ it "should generate the correct projects url" do |
|
53 |
+ expect(@checker.send(:projects_url)).to eq("https://basecamp.com/12345/api/v1/projects.json") |
|
54 |
+ end |
|
39 | 55 |
|
40 | 56 |
it "should not provide the since attribute on first run" do |
41 | 57 |
expect(@checker.send(:query_parameters)).to eq({}) |
@@ -48,6 +64,13 @@ describe Agents::BasecampAgent do |
||
48 | 64 |
expect(@checker.reload.send(:query_parameters)).to eq({:query => {:since => time}}) |
49 | 65 |
end |
50 | 66 |
end |
67 |
+ |
|
68 |
+ describe "#complete_project_id" do |
|
69 |
+ it "should return a array of hashes" do |
|
70 |
+ expect(@checker.complete_project_id).to eq [{text: 'test (1234)', id: 1234}, {text: 'test1 (1235)', id: 1235}] |
|
71 |
+ end |
|
72 |
+ end |
|
73 |
+ |
|
51 | 74 |
describe "#check" do |
52 | 75 |
it "should not emit events on its first run" do |
53 | 76 |
expect { @checker.check }.to change { Event.count }.by(0) |
@@ -60,7 +83,7 @@ describe Agents::BasecampAgent do |
||
60 | 83 |
end |
61 | 84 |
|
62 | 85 |
describe "#working?" do |
63 |
- it "it is working when at least one event was emited" do |
|
86 |
+ it "it is working when at least one event was emitted" do |
|
64 | 87 |
expect(@checker).not_to be_working |
65 | 88 |
@checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00' |
66 | 89 |
@checker.check |
@@ -0,0 +1,42 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::CommanderAgent do |
|
4 |
+ let(:valid_params) { |
|
5 |
+ { |
|
6 |
+ name: 'Example', |
|
7 |
+ schedule: 'every_1h', |
|
8 |
+ options: { |
|
9 |
+ 'action' => 'run', |
|
10 |
+ }, |
|
11 |
+ } |
|
12 |
+ } |
|
13 |
+ |
|
14 |
+ let(:agent) { |
|
15 |
+ described_class.create!(valid_params) { |agent| |
|
16 |
+ agent.user = users(:bob) |
|
17 |
+ } |
|
18 |
+ } |
|
19 |
+ |
|
20 |
+ it_behaves_like AgentControllerConcern |
|
21 |
+ |
|
22 |
+ describe "check!" do |
|
23 |
+ it "should command targets" do |
|
24 |
+ stub(agent).control!.once { nil } |
|
25 |
+ agent.check! |
|
26 |
+ end |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ describe "receive_events" do |
|
30 |
+ it "should command targets" do |
|
31 |
+ stub(agent).control!.once { nil } |
|
32 |
+ |
|
33 |
+ event = Event.new |
|
34 |
+ event.agent = agents(:bob_rain_notifier_agent) |
|
35 |
+ event.payload = { |
|
36 |
+ 'url' => 'http://xkcd.com', |
|
37 |
+ 'link' => 'Random', |
|
38 |
+ } |
|
39 |
+ agent.receive([event]) |
|
40 |
+ end |
|
41 |
+ end |
|
42 |
+end |
@@ -0,0 +1,74 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::DropboxFileUrlAgent do |
|
4 |
+ before(:each) do |
|
5 |
+ @agent = Agents::DropboxFileUrlAgent.new( |
|
6 |
+ name: 'dropbox file to url', |
|
7 |
+ options: {} |
|
8 |
+ ) |
|
9 |
+ @agent.user = users(:bob) |
|
10 |
+ @agent.service = services(:generic) |
|
11 |
+ @agent.save! |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+ it 'cannot be scheduled' do |
|
15 |
+ expect(@agent.cannot_be_scheduled?).to eq true |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ it 'has agent description' do |
|
19 |
+ expect(@agent.description).to_not be_nil |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ it 'has event description' do |
|
23 |
+ expect(@agent.event_description).to_not be_nil |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ describe "#receive" do |
|
27 |
+ |
|
28 |
+ let(:first_dropbox_url_payload) { { 'url' => 'http://dropbox.com/first/path/url' } } |
|
29 |
+ let(:second_dropbox_url_payload) { { 'url' => 'http://dropbox.com/second/path/url' } } |
|
30 |
+ let(:third_dropbox_url_payload) { { 'url' => 'http://dropbox.com/third/path/url' } } |
|
31 |
+ |
|
32 |
+ def create_event(payload) |
|
33 |
+ event = Event.new(payload: payload) |
|
34 |
+ event.agent = agents(:bob_manual_event_agent) |
|
35 |
+ event.save! |
|
36 |
+ event |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ before(:each) do |
|
40 |
+ stub.proxy(Dropbox::API::Client).new do |api| |
|
41 |
+ stub(api).find('/first/path') { stub(Dropbox::API::File.new).direct_url { first_dropbox_url_payload } } |
|
42 |
+ stub(api).find('/second/path') { stub(Dropbox::API::File.new).direct_url { second_dropbox_url_payload } } |
|
43 |
+ stub(api).find('/third/path') { stub(Dropbox::API::File.new).direct_url { third_dropbox_url_payload } } |
|
44 |
+ end |
|
45 |
+ end |
|
46 |
+ |
|
47 |
+ context 'with a single path' do |
|
48 |
+ |
|
49 |
+ before(:each) { @event = create_event(paths: '/first/path') } |
|
50 |
+ |
|
51 |
+ it 'creates one event with the temporary dropbox link' do |
|
52 |
+ expect { @agent.receive([@event]) }.to change(Event, :count).by(1) |
|
53 |
+ expect(Event.last.payload).to eq(first_dropbox_url_payload) |
|
54 |
+ end |
|
55 |
+ |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+ context 'with multiple comma-separated paths' do |
|
59 |
+ |
|
60 |
+ before(:each) { @event = create_event(paths: '/first/path, /second/path, /third/path') } |
|
61 |
+ |
|
62 |
+ it 'creates one event with the temporary dropbox link for each path' do |
|
63 |
+ expect { @agent.receive([@event]) }.to change(Event, :count).by(3) |
|
64 |
+ last_events = Event.last(3) |
|
65 |
+ expect(last_events[0].payload).to eq(first_dropbox_url_payload) |
|
66 |
+ expect(last_events[1].payload).to eq(second_dropbox_url_payload) |
|
67 |
+ expect(last_events[2].payload).to eq(third_dropbox_url_payload) |
|
68 |
+ end |
|
69 |
+ |
|
70 |
+ end |
|
71 |
+ |
|
72 |
+ end |
|
73 |
+ |
|
74 |
+end |
@@ -0,0 +1,173 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::DropboxWatchAgent do |
|
4 |
+ before(:each) do |
|
5 |
+ @agent = Agents::DropboxWatchAgent.new( |
|
6 |
+ name: 'save to dropbox', |
|
7 |
+ options: { |
|
8 |
+ access_token: '70k3n', |
|
9 |
+ dir_to_watch: '/my/dropbox/dir', |
|
10 |
+ expected_update_period_in_days: 2 |
|
11 |
+ } |
|
12 |
+ ) |
|
13 |
+ @agent.user = users(:bob) |
|
14 |
+ @agent.service = services(:generic) |
|
15 |
+ @agent.save! |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ it 'cannot receive events' do |
|
19 |
+ expect(@agent.cannot_receive_events?).to eq true |
|
20 |
+ end |
|
21 |
+ |
|
22 |
+ it 'has agent description' do |
|
23 |
+ expect(@agent.description).to_not be_nil |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ it 'has event description' do |
|
27 |
+ expect(@agent.event_description).to_not be_nil |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ describe '#valid?' do |
|
31 |
+ before(:each) { expect(@agent.valid?).to eq true } |
|
32 |
+ |
|
33 |
+ it 'requires a "dir_to_watch"' do |
|
34 |
+ @agent.options[:dir_to_watch] = nil |
|
35 |
+ expect(@agent.valid?).to eq false |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ describe 'expected_update_period_in_days' do |
|
39 |
+ it 'needs to be present' do |
|
40 |
+ @agent.options[:expected_update_period_in_days] = nil |
|
41 |
+ expect(@agent.valid?).to eq false |
|
42 |
+ end |
|
43 |
+ |
|
44 |
+ it 'needs to be a positive integer' do |
|
45 |
+ @agent.options[:expected_update_period_in_days] = -1 |
|
46 |
+ expect(@agent.valid?).to eq false |
|
47 |
+ end |
|
48 |
+ end |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ describe '#check' do |
|
52 |
+ |
|
53 |
+ let(:first_result) { [{ 'path' => '1.json', 'rev' => '1', 'modified' => '01-01-01' }] } |
|
54 |
+ |
|
55 |
+ before(:each) do |
|
56 |
+ stub.proxy(Dropbox::API::Client).new do |api| |
|
57 |
+ stub(api).ls('/my/dropbox/dir') { first_result } |
|
58 |
+ end |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ it 'saves the directory listing in its memory' do |
|
62 |
+ @agent.check |
|
63 |
+ expect(@agent.memory).to eq 'contents' => first_result |
|
64 |
+ end |
|
65 |
+ |
|
66 |
+ context 'first time' do |
|
67 |
+ |
|
68 |
+ before(:each) { @agent.memory = {} } |
|
69 |
+ |
|
70 |
+ it 'does not send any events' do |
|
71 |
+ expect { @agent.check }.to_not change(Event, :count) |
|
72 |
+ end |
|
73 |
+ |
|
74 |
+ end |
|
75 |
+ |
|
76 |
+ context 'subsequent calls' do |
|
77 |
+ |
|
78 |
+ let(:second_result) { [{ 'path' => '2.json', 'rev' => '1', 'modified' => '02-02-02' }] } |
|
79 |
+ |
|
80 |
+ before(:each) do |
|
81 |
+ @agent.memory = { 'contents' => 'not_empty' } |
|
82 |
+ |
|
83 |
+ stub.proxy(Dropbox::API::Client).new do |api| |
|
84 |
+ stub(api).ls('/my/dropbox/dir') { second_result } |
|
85 |
+ end |
|
86 |
+ end |
|
87 |
+ |
|
88 |
+ it 'sends an event upon a different directory listing' do |
|
89 |
+ payload = { 'diff' => 'object as hash' } |
|
90 |
+ stub.proxy(Agents::DropboxWatchAgent::DropboxDirDiff).new(@agent.memory['contents'], second_result) do |diff| |
|
91 |
+ stub(diff).empty? { false } |
|
92 |
+ stub(diff).to_hash { payload } |
|
93 |
+ end |
|
94 |
+ expect { @agent.check }.to change(Event, :count).by(1) |
|
95 |
+ expect(Event.last.payload).to eq(payload) |
|
96 |
+ end |
|
97 |
+ |
|
98 |
+ it 'does not sent any events when there is no difference on the directory listing' do |
|
99 |
+ stub.proxy(Agents::DropboxWatchAgent::DropboxDirDiff).new(@agent.memory['contents'], second_result) do |diff| |
|
100 |
+ stub(diff).empty? { true } |
|
101 |
+ end |
|
102 |
+ |
|
103 |
+ expect { @agent.check }.to_not change(Event, :count) |
|
104 |
+ end |
|
105 |
+ |
|
106 |
+ end |
|
107 |
+ end |
|
108 |
+ |
|
109 |
+ describe Agents::DropboxWatchAgent::DropboxDirDiff do |
|
110 |
+ |
|
111 |
+ let(:previous) { [ |
|
112 |
+ { 'path' => '1.json', 'rev' => '1' }, |
|
113 |
+ { 'path' => '2.json', 'rev' => '1' }, |
|
114 |
+ { 'path' => '3.json', 'rev' => '1' } |
|
115 |
+ ] } |
|
116 |
+ |
|
117 |
+ let(:current) { [ |
|
118 |
+ { 'path' => '1.json', 'rev' => '2' }, |
|
119 |
+ { 'path' => '3.json', 'rev' => '1' }, |
|
120 |
+ { 'path' => '4.json', 'rev' => '1' } |
|
121 |
+ ] } |
|
122 |
+ |
|
123 |
+ describe '#empty?' do |
|
124 |
+ |
|
125 |
+ it 'is true when no differences are detected' do |
|
126 |
+ diff = Agents::DropboxWatchAgent::DropboxDirDiff.new(previous, previous) |
|
127 |
+ expect(diff.empty?).to eq true |
|
128 |
+ end |
|
129 |
+ |
|
130 |
+ it 'is false when differences were detected' do |
|
131 |
+ diff = Agents::DropboxWatchAgent::DropboxDirDiff.new(previous, current) |
|
132 |
+ expect(diff.empty?).to eq false |
|
133 |
+ end |
|
134 |
+ |
|
135 |
+ end |
|
136 |
+ |
|
137 |
+ describe '#to_hash' do |
|
138 |
+ |
|
139 |
+ subject(:diff_hash) { Agents::DropboxWatchAgent::DropboxDirDiff.new(previous, current).to_hash } |
|
140 |
+ |
|
141 |
+ it 'detects additions' do |
|
142 |
+ expect(diff_hash[:added]).to eq [{ 'path' => '4.json', 'rev' => '1' }] |
|
143 |
+ end |
|
144 |
+ |
|
145 |
+ it 'detects removals' do |
|
146 |
+ expect(diff_hash[:removed]).to eq [ { 'path' => '2.json', 'rev' => '1' } ] |
|
147 |
+ end |
|
148 |
+ |
|
149 |
+ it 'detects updates' do |
|
150 |
+ expect(diff_hash[:updated]).to eq [ { 'path' => '1.json', 'rev' => '2' } ] |
|
151 |
+ end |
|
152 |
+ |
|
153 |
+ context 'when the previous value is not defined' do |
|
154 |
+ it 'considers all additions' do |
|
155 |
+ diff_hash = Agents::DropboxWatchAgent::DropboxDirDiff.new(nil, current).to_hash |
|
156 |
+ expect(diff_hash[:added]).to eq current |
|
157 |
+ expect(diff_hash[:removed]).to eq [] |
|
158 |
+ expect(diff_hash[:updated]).to eq [] |
|
159 |
+ end |
|
160 |
+ end |
|
161 |
+ |
|
162 |
+ context 'when the current value is not defined' do |
|
163 |
+ it 'considers all removals' do |
|
164 |
+ diff_hash = Agents::DropboxWatchAgent::DropboxDirDiff.new(previous, nil).to_hash |
|
165 |
+ expect(diff_hash[:added]).to eq [] |
|
166 |
+ expect(diff_hash[:removed]).to eq previous |
|
167 |
+ expect(diff_hash[:updated]).to eq [] |
|
168 |
+ end |
|
169 |
+ end |
|
170 |
+ end |
|
171 |
+ end |
|
172 |
+ |
|
173 |
+end |
@@ -50,6 +50,31 @@ describe Agents::HipchatAgent do |
||
50 | 50 |
end |
51 | 51 |
end |
52 | 52 |
|
53 |
+ describe "#validate_auth_token" do |
|
54 |
+ it "should return true when valid" do |
|
55 |
+ any_instance_of(HipChat::Client) do |klass| |
|
56 |
+ stub(klass).rooms { true } |
|
57 |
+ end |
|
58 |
+ expect(@checker.validate_auth_token).to be true |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ it "should return false when invalid" do |
|
62 |
+ any_instance_of(HipChat::Client) do |klass| |
|
63 |
+ stub(klass).rooms { raise HipChat::UnknownResponseCode.new } |
|
64 |
+ end |
|
65 |
+ expect(@checker.validate_auth_token).to be false |
|
66 |
+ end |
|
67 |
+ end |
|
68 |
+ |
|
69 |
+ describe "#complete_room_name" do |
|
70 |
+ it "should return a array of hashes" do |
|
71 |
+ any_instance_of(HipChat::Client) do |klass| |
|
72 |
+ stub(klass).rooms { [OpenStruct.new(name: 'test'), OpenStruct.new(name: 'test1')] } |
|
73 |
+ end |
|
74 |
+ expect(@checker.complete_room_name).to eq [{text: 'test', id: 'test'},{text: 'test1', id: 'test1'}] |
|
75 |
+ end |
|
76 |
+ end |
|
77 |
+ |
|
53 | 78 |
describe "#receive" do |
54 | 79 |
it "send a message to the hipchat" do |
55 | 80 |
any_instance_of(HipChat::Room) do |obj| |
@@ -8,7 +8,6 @@ describe Agents::MqttAgent do |
||
8 | 8 |
@error_log = StringIO.new |
9 | 9 |
|
10 | 10 |
@server = MQTT::FakeServer.new(41234, '127.0.0.1') |
11 |
- @server.just_one = true |
|
12 | 11 |
@server.logger = Logger.new(@error_log) |
13 | 12 |
@server.logger.level = Logger::DEBUG |
14 | 13 |
@server.start |
@@ -34,7 +33,14 @@ describe Agents::MqttAgent do |
||
34 | 33 |
end |
35 | 34 |
|
36 | 35 |
describe "#check" do |
37 |
- it "should check that initial run creates an event" do |
|
36 |
+ it "should create events in the initial run" do |
|
37 |
+ expect { @checker.check }.to change { Event.count }.by(2) |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ it "should ignore retained messages that are previously received" do |
|
41 |
+ expect { @checker.check }.to change { Event.count }.by(2) |
|
42 |
+ expect { @checker.check }.to change { Event.count }.by(1) |
|
43 |
+ expect { @checker.check }.to change { Event.count }.by(1) |
|
38 | 44 |
expect { @checker.check }.to change { Event.count }.by(2) |
39 | 45 |
end |
40 | 46 |
end |
@@ -55,6 +55,10 @@ describe Agents::RssAgent do |
||
55 | 55 |
expect { |
56 | 56 |
agent.check |
57 | 57 |
}.to change { agent.events.count }.by(20) |
58 |
+ |
|
59 |
+ event = agent.events.last |
|
60 |
+ expect(event.payload['url']).to eq("https://github.com/cantino/huginn/commit/d0a844662846cf3c83b94c637c1803f03db5a5b0") |
|
61 |
+ expect(event.payload['urls']).to eq(["https://github.com/cantino/huginn/commit/d0a844662846cf3c83b94c637c1803f03db5a5b0"]) |
|
58 | 62 |
end |
59 | 63 |
|
60 | 64 |
it "should track ids and not re-emit the same item when seen again" do |
@@ -1,96 +1,68 @@ |
||
1 | 1 |
require 'spec_helper' |
2 | 2 |
|
3 | 3 |
describe Agents::SchedulerAgent do |
4 |
- before do |
|
5 |
- @agent = Agents::SchedulerAgent.new(name: 'Example', options: { 'schedule' => '0 * * * *' }) |
|
6 |
- @agent.user = users(:bob) |
|
7 |
- @agent.save |
|
8 |
- end |
|
4 |
+ let(:valid_params) { |
|
5 |
+ { |
|
6 |
+ name: 'Example', |
|
7 |
+ options: { |
|
8 |
+ 'action' => 'run', |
|
9 |
+ 'schedule' => '0 * * * *' |
|
10 |
+ }, |
|
11 |
+ } |
|
12 |
+ } |
|
13 |
+ |
|
14 |
+ let(:agent) { |
|
15 |
+ described_class.create!(valid_params) { |agent| |
|
16 |
+ agent.user = users(:bob) |
|
17 |
+ } |
|
18 |
+ } |
|
19 |
+ |
|
20 |
+ it_behaves_like AgentControllerConcern |
|
9 | 21 |
|
10 | 22 |
describe "validation" do |
11 |
- it "should validate action" do |
|
12 |
- ['run', 'enable', 'disable', '', nil].each { |action| |
|
13 |
- @agent.options['action'] = action |
|
14 |
- expect(@agent).to be_valid |
|
15 |
- } |
|
16 |
- |
|
17 |
- ['delete', 1, true].each { |action| |
|
18 |
- @agent.options['action'] = action |
|
19 |
- expect(@agent).not_to be_valid |
|
20 |
- } |
|
21 |
- end |
|
22 |
- |
|
23 | 23 |
it "should validate schedule" do |
24 |
- expect(@agent).to be_valid |
|
24 |
+ expect(agent).to be_valid |
|
25 | 25 |
|
26 |
- @agent.options.delete('schedule') |
|
27 |
- expect(@agent).not_to be_valid |
|
26 |
+ agent.options.delete('schedule') |
|
27 |
+ expect(agent).not_to be_valid |
|
28 | 28 |
|
29 |
- @agent.options['schedule'] = nil |
|
30 |
- expect(@agent).not_to be_valid |
|
29 |
+ agent.options['schedule'] = nil |
|
30 |
+ expect(agent).not_to be_valid |
|
31 | 31 |
|
32 |
- @agent.options['schedule'] = '' |
|
33 |
- expect(@agent).not_to be_valid |
|
32 |
+ agent.options['schedule'] = '' |
|
33 |
+ expect(agent).not_to be_valid |
|
34 | 34 |
|
35 |
- @agent.options['schedule'] = '0' |
|
36 |
- expect(@agent).not_to be_valid |
|
35 |
+ agent.options['schedule'] = '0' |
|
36 |
+ expect(agent).not_to be_valid |
|
37 | 37 |
|
38 |
- @agent.options['schedule'] = '*/15 * * * * * *' |
|
39 |
- expect(@agent).not_to be_valid |
|
38 |
+ agent.options['schedule'] = '*/15 * * * * * *' |
|
39 |
+ expect(agent).not_to be_valid |
|
40 | 40 |
|
41 |
- @agent.options['schedule'] = '*/1 * * * *' |
|
42 |
- expect(@agent).to be_valid |
|
41 |
+ agent.options['schedule'] = '*/1 * * * *' |
|
42 |
+ expect(agent).to be_valid |
|
43 | 43 |
|
44 |
- @agent.options['schedule'] = '*/1 * * *' |
|
45 |
- expect(@agent).not_to be_valid |
|
44 |
+ agent.options['schedule'] = '*/1 * * *' |
|
45 |
+ expect(agent).not_to be_valid |
|
46 | 46 |
|
47 |
- stub(@agent).second_precision_enabled { true } |
|
48 |
- @agent.options['schedule'] = '*/15 * * * * *' |
|
49 |
- expect(@agent).to be_valid |
|
47 |
+ stub(agent).second_precision_enabled { true } |
|
48 |
+ agent.options['schedule'] = '*/15 * * * * *' |
|
49 |
+ expect(agent).to be_valid |
|
50 | 50 |
|
51 |
- stub(@agent).second_precision_enabled { false } |
|
52 |
- @agent.options['schedule'] = '*/10 * * * * *' |
|
53 |
- expect(@agent).not_to be_valid |
|
51 |
+ stub(agent).second_precision_enabled { false } |
|
52 |
+ agent.options['schedule'] = '*/10 * * * * *' |
|
53 |
+ expect(agent).not_to be_valid |
|
54 | 54 |
|
55 |
- @agent.options['schedule'] = '5/30 * * * * *' |
|
56 |
- expect(@agent).not_to be_valid |
|
55 |
+ agent.options['schedule'] = '5/30 * * * * *' |
|
56 |
+ expect(agent).not_to be_valid |
|
57 | 57 |
|
58 |
- @agent.options['schedule'] = '*/15 * * * * *' |
|
59 |
- expect(@agent).to be_valid |
|
58 |
+ agent.options['schedule'] = '*/15 * * * * *' |
|
59 |
+ expect(agent).to be_valid |
|
60 | 60 |
|
61 |
- @agent.options['schedule'] = '15,45 * * * * *' |
|
62 |
- expect(@agent).to be_valid |
|
61 |
+ agent.options['schedule'] = '15,45 * * * * *' |
|
62 |
+ expect(agent).to be_valid |
|
63 | 63 |
|
64 |
- @agent.options['schedule'] = '0 * * * * *' |
|
65 |
- expect(@agent).to be_valid |
|
66 |
- end |
|
67 |
- end |
|
68 |
- |
|
69 |
- describe 'control_action' do |
|
70 |
- it "should be one of the supported values" do |
|
71 |
- ['run', '', nil].each { |action| |
|
72 |
- @agent.options['action'] = action |
|
73 |
- expect(@agent.control_action).to eq('run') |
|
74 |
- } |
|
75 |
- |
|
76 |
- ['enable', 'disable'].each { |action| |
|
77 |
- @agent.options['action'] = action |
|
78 |
- expect(@agent.control_action).to eq(action) |
|
79 |
- } |
|
80 |
- end |
|
81 |
- |
|
82 |
- it "cannot be 'run' if any of the control targets cannot be scheduled" do |
|
83 |
- expect(@agent.control_action).to eq('run') |
|
84 |
- @agent.control_targets = [agents(:bob_rain_notifier_agent)] |
|
85 |
- expect(@agent).not_to be_valid |
|
86 |
- end |
|
87 |
- |
|
88 |
- it "can be 'enable' or 'disable' no matter if control targets can be scheduled or not" do |
|
89 |
- ['enable', 'disable'].each { |action| |
|
90 |
- @agent.options['action'] = action |
|
91 |
- @agent.control_targets = [agents(:bob_rain_notifier_agent)] |
|
92 |
- expect(@agent).to be_valid |
|
93 |
- } |
|
64 |
+ agent.options['schedule'] = '0 * * * * *' |
|
65 |
+ expect(agent).to be_valid |
|
94 | 66 |
end |
95 | 67 |
end |
96 | 68 |
|
@@ -98,43 +70,25 @@ describe Agents::SchedulerAgent do |
||
98 | 70 |
it "should delete memory['scheduled_at'] if and only if options is changed" do |
99 | 71 |
time = Time.now.to_i |
100 | 72 |
|
101 |
- @agent.memory['scheduled_at'] = time |
|
102 |
- @agent.save |
|
103 |
- expect(@agent.memory['scheduled_at']).to eq(time) |
|
73 |
+ agent.memory['scheduled_at'] = time |
|
74 |
+ agent.save |
|
75 |
+ expect(agent.memory['scheduled_at']).to eq(time) |
|
104 | 76 |
|
105 |
- @agent.memory['scheduled_at'] = time |
|
106 |
- # Currently @agent.options[]= is not detected |
|
107 |
- @agent.options = { 'schedule' => '*/5 * * * *' } |
|
108 |
- @agent.save |
|
109 |
- expect(@agent.memory['scheduled_at']).to be_nil |
|
77 |
+ agent.memory['scheduled_at'] = time |
|
78 |
+ # Currently agent.options[]= is not detected |
|
79 |
+ agent.options = { |
|
80 |
+ 'action' => 'run', |
|
81 |
+ 'schedule' => '*/5 * * * *' |
|
82 |
+ } |
|
83 |
+ agent.save |
|
84 |
+ expect(agent.memory['scheduled_at']).to be_nil |
|
110 | 85 |
end |
111 | 86 |
end |
112 | 87 |
|
113 | 88 |
describe "check!" do |
114 | 89 |
it "should control targets" do |
115 |
- control_targets = [agents(:bob_website_agent), agents(:bob_weather_agent)] |
|
116 |
- @agent.control_targets = control_targets |
|
117 |
- @agent.save! |
|
118 |
- |
|
119 |
- control_target_ids = control_targets.map(&:id) |
|
120 |
- stub(Agent).async_check(anything) { |id| |
|
121 |
- control_target_ids.delete(id) |
|
122 |
- } |
|
123 |
- |
|
124 |
- @agent.check! |
|
125 |
- expect(control_target_ids).to be_empty |
|
126 |
- |
|
127 |
- @agent.options['action'] = 'disable' |
|
128 |
- @agent.save! |
|
129 |
- |
|
130 |
- @agent.check! |
|
131 |
- control_targets.all? { |control_target| control_target.disabled? } |
|
132 |
- |
|
133 |
- @agent.options['action'] = 'enable' |
|
134 |
- @agent.save! |
|
135 |
- |
|
136 |
- @agent.check! |
|
137 |
- control_targets.all? { |control_target| !control_target.disabled? } |
|
90 |
+ stub(agent).control!.once { nil } |
|
91 |
+ agent.check! |
|
138 | 92 |
end |
139 | 93 |
end |
140 | 94 |
end |
@@ -15,7 +15,7 @@ describe Service do |
||
15 | 15 |
expect(@service.global).to eq(false) |
16 | 16 |
end |
17 | 17 |
|
18 |
- it "disconnects agents and disables them if the previously global service is made private again", focus: true do |
|
18 |
+ it "disconnects agents and disables them if the previously global service is made private again" do |
|
19 | 19 |
agent = agents(:bob_basecamp_agent) |
20 | 20 |
jane_agent = agents(:jane_basecamp_agent) |
21 | 21 |
|
@@ -0,0 +1,40 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe FormConfigurableAgentPresenter do |
|
4 |
+ class FormConfigurableAgentPresenterAgent < Agent |
|
5 |
+ include FormConfigurable |
|
6 |
+ |
|
7 |
+ form_configurable :string, roles: :validatable |
|
8 |
+ form_configurable :text, type: :text, roles: :completable |
|
9 |
+ form_configurable :boolean, type: :boolean |
|
10 |
+ form_configurable :array, type: :array, values: [1, 2, 3] |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ before(:all) do |
|
14 |
+ @presenter = FormConfigurableAgentPresenter.new(FormConfigurableAgentPresenterAgent.new, ActionController::Base.new.view_context) |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ it "works for the type :string" do |
|
18 |
+ expect(@presenter.option_field_for(:string)).to( |
|
19 |
+ have_tag('input', with: {:'data-attribute' => 'string', role: 'validatable form-configurable', type: 'text', name: 'agent[options][string]'}) |
|
20 |
+ ) |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ it "works for the type :text" do |
|
24 |
+ expect(@presenter.option_field_for(:text)).to( |
|
25 |
+ have_tag('textarea', with: {:'data-attribute' => 'text', role: 'completable form-configurable', name: 'agent[options][text]'}) |
|
26 |
+ ) |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "works for the type :boolean" do |
|
30 |
+ expect(@presenter.option_field_for(:boolean)).to( |
|
31 |
+ have_tag('input', with: {:'data-attribute' => 'boolean', role: 'form-configurable', name: 'agent[options][boolean_radio]', type: 'radio'}) |
|
32 |
+ ) |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ it "works for the type :array" do |
|
36 |
+ expect(@presenter.option_field_for(:array)).to( |
|
37 |
+ have_tag('input', with: {:'data-attribute' => 'array', role: 'completable form-configurable', type: 'text', name: 'agent[options][array]'}) |
|
38 |
+ ) |
|
39 |
+ end |
|
40 |
+end |
@@ -52,7 +52,9 @@ class MQTT::FakeServer |
||
52 | 52 |
@port = @socket.addr[1] |
53 | 53 |
@thread ||= Thread.new do |
54 | 54 |
logger.info "Started a fake MQTT server on #{@address}:#{@port}" |
55 |
+ @times = 0 |
|
55 | 56 |
loop do |
57 |
+ @times += 1 |
|
56 | 58 |
# Wait for a client to connect |
57 | 59 |
client = @socket.accept |
58 | 60 |
@pings_received = 0 |
@@ -103,16 +105,33 @@ class MQTT::FakeServer |
||
103 | 105 |
:granted_qos => 0 |
104 | 106 |
) |
105 | 107 |
topic = packet.topics[0][0] |
106 |
- client.write MQTT::Packet::Publish.new( |
|
107 |
- :topic => topic, |
|
108 |
- :payload => "hello #{topic}", |
|
109 |
- :retain => true |
|
110 |
- ) |
|
111 |
- client.write MQTT::Packet::Publish.new( |
|
112 |
- :topic => topic, |
|
113 |
- :payload => "did you know about #{topic}", |
|
114 |
- :retain => true |
|
115 |
- ) |
|
108 |
+ case @times |
|
109 |
+ when 1, ->x { x >= 3 } |
|
110 |
+ # Deliver retained messages |
|
111 |
+ client.write MQTT::Packet::Publish.new( |
|
112 |
+ :topic => topic, |
|
113 |
+ :payload => "did you know about #{topic}", |
|
114 |
+ :retain => true |
|
115 |
+ ) |
|
116 |
+ client.write MQTT::Packet::Publish.new( |
|
117 |
+ :topic => topic, |
|
118 |
+ :payload => "hello #{topic}", |
|
119 |
+ :retain => true |
|
120 |
+ ) |
|
121 |
+ when 2 |
|
122 |
+ # Deliver a still retained message |
|
123 |
+ client.write MQTT::Packet::Publish.new( |
|
124 |
+ :topic => topic, |
|
125 |
+ :payload => "hello #{topic}", |
|
126 |
+ :retain => true |
|
127 |
+ ) |
|
128 |
+ # Deliver a fresh message |
|
129 |
+ client.write MQTT::Packet::Publish.new( |
|
130 |
+ :topic => topic, |
|
131 |
+ :payload => "did you know about #{topic}", |
|
132 |
+ :retain => false |
|
133 |
+ ) |
|
134 |
+ end |
|
116 | 135 |
|
117 | 136 |
when MQTT::Packet::Pingreq |
118 | 137 |
client.write MQTT::Packet::Pingresp.new |
@@ -134,4 +153,4 @@ if __FILE__ == $0 |
||
134 | 153 |
server = MQTT::FakeServer.new(MQTT::DEFAULT_PORT) |
135 | 154 |
server.logger.level = Logger::DEBUG |
136 | 155 |
server.run |
137 |
-end |
|
156 |
+end |
@@ -0,0 +1,111 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for AgentControllerConcern do |
|
4 |
+ describe "preconditions" do |
|
5 |
+ it "must be satisfied for these shared examples" do |
|
6 |
+ expect(agent.user).to eq(users(:bob)) |
|
7 |
+ expect(agent.control_action).to eq('run') |
|
8 |
+ end |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ describe "validation" do |
|
12 |
+ describe "of action" do |
|
13 |
+ it "should allow certain values" do |
|
14 |
+ ['run', 'enable', 'disable', '{{ action }}'].each { |action| |
|
15 |
+ agent.options['action'] = action |
|
16 |
+ expect(agent).to be_valid |
|
17 |
+ } |
|
18 |
+ end |
|
19 |
+ |
|
20 |
+ it "should disallow obviously bad values" do |
|
21 |
+ ['delete', nil, 1, true].each { |action| |
|
22 |
+ agent.options['action'] = action |
|
23 |
+ expect(agent).not_to be_valid |
|
24 |
+ } |
|
25 |
+ end |
|
26 |
+ |
|
27 |
+ it "should accept 'run' if all target agents are schedulable" do |
|
28 |
+ agent.control_targets = [agents(:bob_website_agent)] |
|
29 |
+ expect(agent).to be_valid |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ it "should reject 'run' if targets include an unschedulable agent" do |
|
33 |
+ agent.control_targets = [agents(:bob_rain_notifier_agent)] |
|
34 |
+ expect(agent).not_to be_valid |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ it "should not reject 'enable' or 'disable' no matter if targets include an unschedulable agent" do |
|
38 |
+ ['enable', 'disable'].each { |action| |
|
39 |
+ agent.options['action'] = action |
|
40 |
+ agent.control_targets = [agents(:bob_rain_notifier_agent)] |
|
41 |
+ expect(agent).to be_valid |
|
42 |
+ } |
|
43 |
+ end |
|
44 |
+ end |
|
45 |
+ end |
|
46 |
+ |
|
47 |
+ describe 'control_action' do |
|
48 |
+ it "returns options['action']" do |
|
49 |
+ expect(agent.control_action).to eq('run') |
|
50 |
+ |
|
51 |
+ ['run', 'enable', 'disable'].each { |action| |
|
52 |
+ agent.options['action'] = action |
|
53 |
+ expect(agent.control_action).to eq(action) |
|
54 |
+ } |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ it "returns the result of interpolation" do |
|
58 |
+ expect(agent.control_action).to eq('run') |
|
59 |
+ |
|
60 |
+ agent.options['action'] = '{{ "enable" }}' |
|
61 |
+ expect(agent.control_action).to eq('enable') |
|
62 |
+ end |
|
63 |
+ end |
|
64 |
+ |
|
65 |
+ describe "control!" do |
|
66 |
+ before do |
|
67 |
+ agent.control_targets = [agents(:bob_website_agent), agents(:bob_weather_agent)] |
|
68 |
+ agent.save! |
|
69 |
+ end |
|
70 |
+ |
|
71 |
+ it "should run targets" do |
|
72 |
+ control_target_ids = agent.control_targets.map(&:id) |
|
73 |
+ stub(Agent).async_check(anything) { |id| |
|
74 |
+ control_target_ids.delete(id) |
|
75 |
+ } |
|
76 |
+ |
|
77 |
+ agent.control! |
|
78 |
+ expect(control_target_ids).to be_empty |
|
79 |
+ end |
|
80 |
+ |
|
81 |
+ it "should not run disabled targets" do |
|
82 |
+ control_target_ids = agent.control_targets.map(&:id) |
|
83 |
+ stub(Agent).async_check(anything) { |id| |
|
84 |
+ control_target_ids.delete(id) |
|
85 |
+ } |
|
86 |
+ |
|
87 |
+ agent.control_targets.last.update!(disabled: true) |
|
88 |
+ |
|
89 |
+ agent.control! |
|
90 |
+ expect(control_target_ids).to eq [agent.control_targets.last.id] |
|
91 |
+ end |
|
92 |
+ |
|
93 |
+ it "should enable targets" do |
|
94 |
+ agent.options['action'] = 'disable' |
|
95 |
+ agent.save! |
|
96 |
+ agent.control_targets.first.update!(disabled: true) |
|
97 |
+ |
|
98 |
+ agent.control! |
|
99 |
+ expect(agent.control_targets.reload).to all(be_disabled) |
|
100 |
+ end |
|
101 |
+ |
|
102 |
+ it "should disable targets" do |
|
103 |
+ agent.options['action'] = 'enable' |
|
104 |
+ agent.save! |
|
105 |
+ agent.control_targets.first.update!(disabled: true) |
|
106 |
+ |
|
107 |
+ agent.control! |
|
108 |
+ expect(agent.control_targets.reload).to all(satisfy { |a| !a.disabled? }) |
|
109 |
+ end |
|
110 |
+ end |
|
111 |
+end |
@@ -0,0 +1,40 @@ |
||
1 |
+// |
|
2 |
+// Use internal $.serializeArray to get list of form elements which is |
|
3 |
+// consistent with $.serialize |
|
4 |
+// |
|
5 |
+// From version 2.0.0, $.serializeObject will stop converting [name] values |
|
6 |
+// to camelCase format. This is *consistent* with other serialize methods: |
|
7 |
+// |
|
8 |
+// - $.serialize |
|
9 |
+// - $.serializeArray |
|
10 |
+// |
|
11 |
+// If you require camel casing, you can either download version 1.0.4 or map |
|
12 |
+// them yourself. |
|
13 |
+// |
|
14 |
+ |
|
15 |
+(function($){ |
|
16 |
+ $.fn.serializeObject = function () { |
|
17 |
+ "use strict"; |
|
18 |
+ |
|
19 |
+ var result = {}; |
|
20 |
+ var extend = function (i, element) { |
|
21 |
+ var node = result[element.name]; |
|
22 |
+ |
|
23 |
+ // If node with same name exists already, need to convert it to an array as it |
|
24 |
+ // is a multi-value field (i.e., checkboxes) |
|
25 |
+ |
|
26 |
+ if ('undefined' !== typeof node && node !== null) { |
|
27 |
+ if ($.isArray(node)) { |
|
28 |
+ node.push(element.value); |
|
29 |
+ } else { |
|
30 |
+ result[element.name] = [node, element.value]; |
|
31 |
+ } |
|
32 |
+ } else { |
|
33 |
+ result[element.name] = element.value; |
|
34 |
+ } |
|
35 |
+ }; |
|
36 |
+ |
|
37 |
+ $.each(this.serializeArray(), extend); |
|
38 |
+ return result; |
|
39 |
+ }; |
|
40 |
+})(jQuery); |